appattest-checker-node
Version:
Node.JS library to check/verify iOS App Attest attestations & assertions
112 lines • 4.56 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseAssertion = exports.verifySignaturePerStep1To3 = exports.verifyRPIdPerStep4 = exports.verifyAssertion = void 0;
const buffer_1 = require("buffer");
const crypto_1 = require("crypto");
const cbor_1 = __importDefault(require("cbor"));
const utils_1 = require("./utils");
const STEPS = [
verifySignaturePerStep1To3,
verifyRPIdPerStep4,
];
/**
* Verify an Assertion generated on an iOS device using DCAppAttestService per steps 1-4
* {@link https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server#3576644 | here}.
*
* @remark This code does not verify that any challenge inluded in clientDataHash is valid. Calling
* code should do that. Also, on successful verification, the signCount from the Assertion is
* returned. Calling code should check that it exceeds any previous persisted signCount and persist
* the returned value. These two points are mentioned in Steps 5 & 6 from steps above.
*
* @remark Ensure that clientDataHash is computed from the same request that was used by the client
* for assertion. Any formatting changes could result in issues.
*
* @param clientDataHash SHA256 of the client data (request).
* @param publicKeyPem Public Key of the key pair from the device.
* @param appId App Id that generated the assertion.
* @param assertion Assertion bytes sent up from the device; derived on device by signing
* clientDataHash with private key on the device.
* @returns Result object containing signCount if assertion was verified or error if it was not
* verified.
*/
async function verifyAssertion(clientDataHash, publicKeyPem, appId, assertion) {
const parseResult = await parseAssertion(assertion);
if (typeof parseResult === 'string') {
return { verifyError: 'fail_parsing_assertion', errorMessage: parseResult };
}
const inputs = {
clientDataHash,
publicKeyPem,
appId,
parsedAssertion: parseResult,
};
for (const step of STEPS) {
const error = await step(inputs);
if (error != null) {
return {
verifyError: error,
};
}
}
return {
signCount: (0, utils_1.getSignCount)(inputs.parsedAssertion.authData),
};
}
exports.verifyAssertion = verifyAssertion;
/** @internal */
async function verifyRPIdPerStep4(inputs) {
const rpIdHash = (0, utils_1.getRPIdHash)(inputs.parsedAssertion.authData);
const appIdHash = await (0, utils_1.getSHA256)(buffer_1.Buffer.from(inputs.appId));
return rpIdHash.equals(appIdHash) ? null : 'fail_rpId_mismatch';
}
exports.verifyRPIdPerStep4 = verifyRPIdPerStep4;
/** @internal */
async function verifySignaturePerStep1To3(inputs) {
const noncePrep = buffer_1.Buffer.concat([
inputs.parsedAssertion.authData,
inputs.clientDataHash,
]);
const nonce = await (0, utils_1.getSHA256)(noncePrep);
let publicKey;
try {
publicKey = (0, crypto_1.createPublicKey)(inputs.publicKeyPem);
}
catch (e) {
return 'fail_invalid_publicKey';
}
const verifier = (0, crypto_1.createVerify)('RSA-SHA256');
verifier.update(nonce);
const verified = verifier.verify(publicKey, inputs.parsedAssertion.signature);
return verified ? null : 'fail_signature_verification';
}
exports.verifySignaturePerStep1To3 = verifySignaturePerStep1To3;
// Need to read upto signCount field at offset 33.
const MIN_REQUIRED_ASSERTION_BYTES = 34;
/** @internal */
async function parseAssertion(assertion) {
try {
const assertionObj = await cbor_1.default.decodeFirst(assertion);
const { signature, authenticatorData } = assertionObj;
if (!(signature instanceof buffer_1.Buffer)) {
return 'Invalid `signature` field in Assertion';
}
if (!(authenticatorData instanceof buffer_1.Buffer)) {
return 'Invalid `authenticatorData` field in Assertion';
}
if (authenticatorData.length < MIN_REQUIRED_ASSERTION_BYTES) {
return 'authenticatorData has < 34 bytes';
}
return {
signature,
authData: authenticatorData,
};
}
catch (e) {
return 'Unable to parse CBOR contents from Assertion';
}
}
exports.parseAssertion = parseAssertion;
//# sourceMappingURL=assertion.js.map