UNPKG

@simplewebauthn/server

Version:
217 lines (216 loc) 11 kB
"use strict"; 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, }, }; }