mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
216 lines
11 KiB
216 lines
11 KiB
"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,
|
|
},
|
|
};
|
|
}
|
|
|