@simplewebauthn/server
Version:
SimpleWebAuthn for Servers
217 lines (216 loc) • 11 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyRegistrationResponse = verifyRegistrationResponse;
const decodeAttestationObject_js_1 = require("../helpers/decodeAttestationObject.js");
const decodeClientDataJSON_js_1 = require("../helpers/decodeClientDataJSON.js");
const parseAuthenticatorData_js_1 = require("../helpers/parseAuthenticatorData.js");
const toHash_js_1 = require("../helpers/toHash.js");
const decodeCredentialPublicKey_js_1 = require("../helpers/decodeCredentialPublicKey.js");
const cose_js_1 = require("../helpers/cose.js");
const convertAAGUIDToString_js_1 = require("../helpers/convertAAGUIDToString.js");
const parseBackupFlags_js_1 = require("../helpers/parseBackupFlags.js");
const matchExpectedRPID_js_1 = require("../helpers/matchExpectedRPID.js");
const index_js_1 = require("../helpers/iso/index.js");
const settingsService_js_1 = require("../services/settingsService.js");
const generateRegistrationOptions_js_1 = require("./generateRegistrationOptions.js");
const verifyAttestationFIDOU2F_js_1 = require("./verifications/verifyAttestationFIDOU2F.js");
const verifyAttestationPacked_js_1 = require("./verifications/verifyAttestationPacked.js");
const verifyAttestationAndroidSafetyNet_js_1 = require("./verifications/verifyAttestationAndroidSafetyNet.js");
const verifyAttestationTPM_js_1 = require("./verifications/tpm/verifyAttestationTPM.js");
const verifyAttestationAndroidKey_js_1 = require("./verifications/verifyAttestationAndroidKey.js");
const verifyAttestationApple_js_1 = require("./verifications/verifyAttestationApple.js");
/**
* 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 requireUserPresence **(Optional)** - Enforce user presence by the authenticator (or skip it during auto registration) Defaults to `true`
* @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
* @param attestationSafetyNetEnforceCTSCheck **(Optional)** - Require that an Android device's system integrity has not been tampered with if it uses SafetyNet attestation. Defaults to `true`
*/
async function verifyRegistrationResponse(options) {
const { response, expectedChallenge, expectedOrigin, expectedRPID, expectedType, requireUserPresence = true, requireUserVerification = true, supportedAlgorithmIDs = generateRegistrationOptions_js_1.supportedCOSEAlgorithmIdentifiers, attestationSafetyNetEnforceCTSCheck = true, } = 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 = (0, decodeClientDataJSON_js_1.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 = index_js_1.isoBase64URL.toBuffer(attestationResponse.attestationObject);
const decodedAttestationObject = (0, decodeAttestationObject_js_1.decodeAttestationObject)(attestationObject);
const fmt = decodedAttestationObject.get('fmt');
const authData = decodedAttestationObject.get('authData');
const attStmt = decodedAttestationObject.get('attStmt');
const parsedAuthData = (0, parseAuthenticatorData_js_1.parseAuthenticatorData)(authData);
const { aaguid, rpIdHash, flags, credentialID, counter, credentialPublicKey, extensionsData, } = parsedAuthData;
// Make sure the response's RP ID is ours
let matchedRPID;
if (expectedRPID) {
let expectedRPIDs = [];
if (typeof expectedRPID === 'string') {
expectedRPIDs = [expectedRPID];
}
else {
expectedRPIDs = expectedRPID;
}
matchedRPID = await (0, matchExpectedRPID_js_1.matchExpectedRPID)(rpIdHash, expectedRPIDs);
}
// Make sure someone was physically present
if (requireUserPresence && !flags.up) {
throw new Error('User presence was required, but user was not present');
}
// Enforce user verification if specified
if (requireUserVerification && !flags.uv) {
throw new Error('User verification was 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 = (0, decodeCredentialPublicKey_js_1.decodeCredentialPublicKey)(credentialPublicKey);
const alg = decodedPublicKey.get(cose_js_1.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)) {
const supported = supportedAlgorithmIDs.join(', ');
throw new Error(`Unexpected public key alg "${alg}", expected one of "${supported}"`);
}
const clientDataHash = await (0, toHash_js_1.toHash)(index_js_1.isoBase64URL.toBuffer(attestationResponse.clientDataJSON));
const rootCertificates = settingsService_js_1.SettingsService.getRootCertificates({
identifier: fmt,
});
// Prepare arguments to pass to the relevant verification method
const verifierOpts = {
aaguid,
attStmt,
authData,
clientDataHash,
credentialID,
credentialPublicKey,
rootCertificates,
rpIdHash,
attestationSafetyNetEnforceCTSCheck,
};
/**
* Verification can only be performed when attestation = 'direct'
*/
let verified = false;
if (fmt === 'fido-u2f') {
verified = await (0, verifyAttestationFIDOU2F_js_1.verifyAttestationFIDOU2F)(verifierOpts);
}
else if (fmt === 'packed') {
verified = await (0, verifyAttestationPacked_js_1.verifyAttestationPacked)(verifierOpts);
}
else if (fmt === 'android-safetynet') {
verified = await (0, verifyAttestationAndroidSafetyNet_js_1.verifyAttestationAndroidSafetyNet)(verifierOpts);
}
else if (fmt === 'android-key') {
verified = await (0, verifyAttestationAndroidKey_js_1.verifyAttestationAndroidKey)(verifierOpts);
}
else if (fmt === 'tpm') {
verified = await (0, verifyAttestationTPM_js_1.verifyAttestationTPM)(verifierOpts);
}
else if (fmt === 'apple') {
verified = await (0, verifyAttestationApple_js_1.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}`);
}
if (!verified) {
return { verified: false };
}
const { credentialDeviceType, credentialBackedUp } = (0, parseBackupFlags_js_1.parseBackupFlags)(flags);
return {
verified: true,
registrationInfo: {
fmt,
aaguid: (0, convertAAGUIDToString_js_1.convertAAGUIDToString)(aaguid),
credentialType,
credential: {
id: index_js_1.isoBase64URL.fromBuffer(credentialID),
publicKey: credentialPublicKey,
counter,
transports: response.response.transports,
},
attestationObject,
userVerified: flags.uv,
credentialDeviceType,
credentialBackedUp,
origin: clientDataJSON.origin,
rpID: matchedRPID,
authenticatorExtensionResults: extensionsData,
},
};
}