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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
|
import type {
Base64URLString,
COSEAlgorithmIdentifier,
CredentialDeviceType,
RegistrationResponseJSON,
} from '../deps.ts';
import {
AttestationFormat,
AttestationStatement,
decodeAttestationObject,
} from '../helpers/decodeAttestationObject.ts';
import { AuthenticationExtensionsAuthenticatorOutputs } from '../helpers/decodeAuthenticatorExtensions.ts';
import { decodeClientDataJSON } from '../helpers/decodeClientDataJSON.ts';
import { parseAuthenticatorData } from '../helpers/parseAuthenticatorData.ts';
import { toHash } from '../helpers/toHash.ts';
import { decodeCredentialPublicKey } from '../helpers/decodeCredentialPublicKey.ts';
import { COSEKEYS } from '../helpers/cose.ts';
import { convertAAGUIDToString } from '../helpers/convertAAGUIDToString.ts';
import { parseBackupFlags } from '../helpers/parseBackupFlags.ts';
import { matchExpectedRPID } from '../helpers/matchExpectedRPID.ts';
import { isoBase64URL } from '../helpers/iso/index.ts';
import { SettingsService } from '../services/settingsService.ts';
import { supportedCOSEAlgorithmIdentifiers } from './generateRegistrationOptions.ts';
import { verifyAttestationFIDOU2F } from './verifications/verifyAttestationFIDOU2F.ts';
import { verifyAttestationPacked } from './verifications/verifyAttestationPacked.ts';
import { verifyAttestationAndroidSafetyNet } from './verifications/verifyAttestationAndroidSafetyNet.ts';
import { verifyAttestationTPM } from './verifications/tpm/verifyAttestationTPM.ts';
import { verifyAttestationAndroidKey } from './verifications/verifyAttestationAndroidKey.ts';
import { verifyAttestationApple } from './verifications/verifyAttestationApple.ts';
export type VerifyRegistrationResponseOpts = {
response: RegistrationResponseJSON;
expectedChallenge: string | ((challenge: string) => boolean | Promise<boolean>);
expectedOrigin: string | string[];
expectedRPID?: string | string[];
expectedType?: string | string[];
requireUserVerification?: boolean;
supportedAlgorithmIDs?: COSEAlgorithmIdentifier[];
};
/**
* Verify that the user has legitimately completed the registration process
*
* **Options:**
*
* @param response - Response returned by **@simplewebauthn/browser**'s `startAuthentication()`
* @param expectedChallenge - The base64url-encoded `options.challenge` returned by `generateRegistrationOptions()`
* @param expectedOrigin - Website URL (or array of URLs) that the registration should have occurred on
* @param expectedRPID - RP ID (or array of IDs) that was specified in the registration options
* @param expectedType **(Optional)** - The response type expected ('webauthn.create')
* @param requireUserVerification **(Optional)** - Enforce user verification by the authenticator (via PIN, fingerprint, etc...) Defaults to `true`
* @param supportedAlgorithmIDs **(Optional)** - Array of numeric COSE algorithm identifiers supported for attestation by this RP. See https://www.iana.org/assignments/cose/cose.xhtml#algorithms. Defaults to all supported algorithm IDs
*/
export async function verifyRegistrationResponse(
options: VerifyRegistrationResponseOpts,
): Promise<VerifiedRegistrationResponse> {
const {
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
expectedType,
requireUserVerification = true,
supportedAlgorithmIDs = supportedCOSEAlgorithmIdentifiers,
} = options;
const { id, rawId, type: credentialType, response: attestationResponse } = response;
// Ensure credential specified an ID
if (!id) {
throw new Error('Missing credential ID');
}
// Ensure ID is base64url-encoded
if (id !== rawId) {
throw new Error('Credential ID was not base64url-encoded');
}
// Make sure credential type is public-key
if (credentialType !== 'public-key') {
throw new Error(
`Unexpected credential type ${credentialType}, expected "public-key"`,
);
}
const clientDataJSON = decodeClientDataJSON(
attestationResponse.clientDataJSON,
);
const { type, origin, challenge, tokenBinding } = clientDataJSON;
// Make sure we're handling an registration
if (Array.isArray(expectedType)) {
if (!expectedType.includes(type)) {
const joinedExpectedType = expectedType.join(', ');
throw new Error(
`Unexpected registration response type "${type}", expected one of: ${joinedExpectedType}`,
);
}
} else if (expectedType) {
if (type !== expectedType) {
throw new Error(
`Unexpected registration response type "${type}", expected "${expectedType}"`,
);
}
} else if (type !== 'webauthn.create') {
throw new Error(`Unexpected registration response type: ${type}`);
}
// Ensure the device provided the challenge we gave it
if (typeof expectedChallenge === 'function') {
if (!(await expectedChallenge(challenge))) {
throw new Error(
`Custom challenge verifier returned false for registration response challenge "${challenge}"`,
);
}
} else if (challenge !== expectedChallenge) {
throw new Error(
`Unexpected registration response challenge "${challenge}", expected "${expectedChallenge}"`,
);
}
// Check that the origin is our site
if (Array.isArray(expectedOrigin)) {
if (!expectedOrigin.includes(origin)) {
throw new Error(
`Unexpected registration response origin "${origin}", expected one of: ${
expectedOrigin.join(
', ',
)
}`,
);
}
} else {
if (origin !== expectedOrigin) {
throw new Error(
`Unexpected registration response origin "${origin}", expected "${expectedOrigin}"`,
);
}
}
if (tokenBinding) {
if (typeof tokenBinding !== 'object') {
throw new Error(`Unexpected value for TokenBinding "${tokenBinding}"`);
}
if (
['present', 'supported', 'not-supported'].indexOf(tokenBinding.status) < 0
) {
throw new Error(
`Unexpected tokenBinding.status value of "${tokenBinding.status}"`,
);
}
}
const attestationObject = isoBase64URL.toBuffer(
attestationResponse.attestationObject,
);
const decodedAttestationObject = decodeAttestationObject(attestationObject);
const fmt = decodedAttestationObject.get('fmt');
const authData = decodedAttestationObject.get('authData');
const attStmt = decodedAttestationObject.get('attStmt');
const parsedAuthData = parseAuthenticatorData(authData);
const {
aaguid,
rpIdHash,
flags,
credentialID,
counter,
credentialPublicKey,
extensionsData,
} = parsedAuthData;
// Make sure the response's RP ID is ours
let matchedRPID: string | undefined;
if (expectedRPID) {
let expectedRPIDs: string[] = [];
if (typeof expectedRPID === 'string') {
expectedRPIDs = [expectedRPID];
} else {
expectedRPIDs = expectedRPID;
}
matchedRPID = await matchExpectedRPID(rpIdHash, expectedRPIDs);
}
// Make sure someone was physically present
if (!flags.up) {
throw new Error('User not present during registration');
}
// Enforce user verification if specified
if (requireUserVerification && !flags.uv) {
throw new Error(
'User verification required, but user could not be verified',
);
}
if (!credentialID) {
throw new Error('No credential ID was provided by authenticator');
}
if (!credentialPublicKey) {
throw new Error('No public key was provided by authenticator');
}
if (!aaguid) {
throw new Error('No AAGUID was present during registration');
}
const decodedPublicKey = decodeCredentialPublicKey(credentialPublicKey);
const alg = decodedPublicKey.get(COSEKEYS.alg);
if (typeof alg !== 'number') {
throw new Error('Credential public key was missing numeric alg');
}
// Make sure the key algorithm is one we specified within the registration options
if (!supportedAlgorithmIDs.includes(alg as number)) {
const supported = supportedAlgorithmIDs.join(', ');
throw new Error(
`Unexpected public key alg "${alg}", expected one of "${supported}"`,
);
}
const clientDataHash = await toHash(
isoBase64URL.toBuffer(attestationResponse.clientDataJSON),
);
const rootCertificates = SettingsService.getRootCertificates({
identifier: fmt,
});
// Prepare arguments to pass to the relevant verification method
const verifierOpts: AttestationFormatVerifierOpts = {
aaguid,
attStmt,
authData,
clientDataHash,
credentialID,
credentialPublicKey,
rootCertificates,
rpIdHash,
};
/**
* Verification can only be performed when attestation = 'direct'
*/
let verified = false;
if (fmt === 'fido-u2f') {
verified = await verifyAttestationFIDOU2F(verifierOpts);
} else if (fmt === 'packed') {
verified = await verifyAttestationPacked(verifierOpts);
} else if (fmt === 'android-safetynet') {
verified = await verifyAttestationAndroidSafetyNet(verifierOpts);
} else if (fmt === 'android-key') {
verified = await verifyAttestationAndroidKey(verifierOpts);
} else if (fmt === 'tpm') {
verified = await verifyAttestationTPM(verifierOpts);
} else if (fmt === 'apple') {
verified = await verifyAttestationApple(verifierOpts);
} else if (fmt === 'none') {
if (attStmt.size > 0) {
throw new Error('None attestation had unexpected attestation statement');
}
// This is the weaker of the attestations, so there's nothing else to really check
verified = true;
} else {
throw new Error(`Unsupported Attestation Format: ${fmt}`);
}
const toReturn: VerifiedRegistrationResponse = {
verified,
};
if (toReturn.verified) {
const { credentialDeviceType, credentialBackedUp } = parseBackupFlags(
flags,
);
toReturn.registrationInfo = {
fmt,
counter,
aaguid: convertAAGUIDToString(aaguid),
credentialID: isoBase64URL.fromBuffer(credentialID),
credentialPublicKey,
credentialType,
attestationObject,
userVerified: flags.uv,
credentialDeviceType,
credentialBackedUp,
origin: clientDataJSON.origin,
rpID: matchedRPID,
authenticatorExtensionResults: extensionsData,
};
}
return toReturn;
}
/**
* Result of registration verification
*
* @param verified If the assertion response could be verified
* @param registrationInfo.fmt Type of attestation
* @param registrationInfo.counter The number of times the authenticator reported it has been used.
* **Should be kept in a DB for later reference to help prevent replay attacks!**
* @param registrationInfo.aaguid Authenticator's Attestation GUID indicating the type of the
* authenticator
* @param registrationInfo.credentialPublicKey The credential's public key
* @param registrationInfo.credentialID The credential's credential ID for the public key above
* @param registrationInfo.credentialType The type of the credential returned by the browser
* @param registrationInfo.userVerified Whether the user was uniquely identified during attestation
* @param registrationInfo.attestationObject The raw `response.attestationObject` Buffer returned by
* the authenticator
* @param registrationInfo.credentialDeviceType Whether this is a single-device or multi-device
* credential. **Should be kept in a DB for later reference!**
* @param registrationInfo.credentialBackedUp Whether or not the multi-device credential has been
* backed up. Always `false` for single-device credentials. **Should be kept in a DB for later
* reference!**
* @param registrationInfo.origin The origin of the website that the registration occurred on
* @param registrationInfo?.rpID The RP ID that the registration occurred on, if one or more were
* specified in the registration options
* @param registrationInfo?.authenticatorExtensionResults The authenticator extensions returned
* by the browser
*/
export type VerifiedRegistrationResponse = {
verified: boolean;
registrationInfo?: {
fmt: AttestationFormat;
counter: number;
aaguid: string;
credentialID: Base64URLString;
credentialPublicKey: Uint8Array;
credentialType: 'public-key';
attestationObject: Uint8Array;
userVerified: boolean;
credentialDeviceType: CredentialDeviceType;
credentialBackedUp: boolean;
origin: string;
rpID?: string;
authenticatorExtensionResults?: AuthenticationExtensionsAuthenticatorOutputs;
};
};
/**
* Values passed to all attestation format verifiers, from which they are free to use as they please
*/
export type AttestationFormatVerifierOpts = {
aaguid: Uint8Array;
attStmt: AttestationStatement;
authData: Uint8Array;
clientDataHash: Uint8Array;
credentialID: Uint8Array;
credentialPublicKey: Uint8Array;
rootCertificates: string[];
rpIdHash: Uint8Array;
verifyTimestampMS?: boolean;
};
|