From 605f1720179460bab88671515652749c3149314e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karlo=20Ben=C4=8Di=C4=87?= Date: Wed, 15 Jan 2025 19:32:24 +0100 Subject: [PATCH] Add unit tests for v2 and v3 BMFF hash assertions --- src/manifest/assertions/BMFFHashAssertion.ts | 9 +- .../assertions/BMFFHashAssertion.test.ts | 88 ++++++++++++++++++- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/manifest/assertions/BMFFHashAssertion.ts b/src/manifest/assertions/BMFFHashAssertion.ts index be10cb0..00b234b 100644 --- a/src/manifest/assertions/BMFFHashAssertion.ts +++ b/src/manifest/assertions/BMFFHashAssertion.ts @@ -320,21 +320,24 @@ export class BMFFHashAssertion extends Assertion { private async getChunks(asset: BMFF, tree: RawMerkleMap): Promise { const chunks: Uint8Array[] = []; - const mdatBox = asset.getBoxByPath('mdat'); + const mdatBox = asset.getBoxByPath('/mdat'); if (!mdatBox) { throw new ValidationError(ValidationStatusCode.AssertionBMFFHashMismatch, this.sourceBox, 'mdat not found'); } + // Start from the data portion of mdat (after 8-byte header) + const dataOffset = mdatBox.offset + 8; + // Per spec 15.12.2: Handle fixed and variable size blocks if (tree.fixedBlockSize) { - let offset = mdatBox.offset; + let offset = dataOffset; for (let i = 0; i < tree.count; i++) { chunks.push(await asset.getDataRange(offset, tree.fixedBlockSize)); offset += tree.fixedBlockSize; } } else if (tree.variableBlockSizes) { - let offset = mdatBox.offset; + let offset = dataOffset; for (const size of tree.variableBlockSizes) { chunks.push(await asset.getDataRange(offset, size)); offset += size; diff --git a/tests/manifest/assertions/BMFFHashAssertion.test.ts b/tests/manifest/assertions/BMFFHashAssertion.test.ts index d0e055b..3c8c89a 100644 --- a/tests/manifest/assertions/BMFFHashAssertion.test.ts +++ b/tests/manifest/assertions/BMFFHashAssertion.test.ts @@ -1,24 +1,36 @@ /* eslint-disable @typescript-eslint/dot-notation */ import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import path from 'node:path'; import { BMFF, BMFFBox } from '../../../src/asset'; +import { Crypto } from '../../../src/crypto'; import { HashAlgorithm } from '../../../src/crypto/types'; import { CBORBox, DescriptionBox, SuperBox } from '../../../src/jumbf'; import { BMFFHashAssertion, Claim, ValidationStatusCode } from '../../../src/manifest'; import { AssertionLabels } from '../../../src/manifest/assertions/AssertionLabels'; import * as raw from '../../../src/manifest/rawTypes'; +const baseDir = 'tests/fixtures'; + function createBMFFMock(): Uint8Array { /* prettier-ignore */ - const bmffHeader = new Uint8Array([ + const data = new Uint8Array([ + // ftyp box (24 bytes total) 0x00, 0x00, 0x00, 0x18, // Box size (24 bytes) 0x66, 0x74, 0x79, 0x70, // Box type 'ftyp' 0x68, 0x65, 0x69, 0x63, // Major brand 'heic' 0x00, 0x00, 0x00, 0x01, // Minor version 0x68, 0x65, 0x69, 0x63, // Compatible brands 'heic' - 0x6d, 0x69, 0x66, 0x31 // Compatible brands 'mif1' + 0x6d, 0x69, 0x66, 0x31, // Compatible brands 'mif1' + + // mdat box (16 bytes total) + 0x00, 0x00, 0x00, 0x10, // Box size (16 bytes) + 0x6d, 0x64, 0x61, 0x74, // Box type 'mdat' + 0x00, 0x01, 0x02, 0x03, // Data block 1 (4 bytes) + 0x04, 0x05, 0x06, 0x07, // Data block 2 (4 bytes) ]); - return bmffHeader; + return data; } describe('BMFFHashAssertion Tests', function () { @@ -191,4 +203,74 @@ describe('BMFFHashAssertion Tests', function () { assert.equal(result.isValid, false); assert.equal(result.statusEntries[0].code, ValidationStatusCode.AssertionBMFFHashMismatch); }); + + it('should correctly hash HEIC file with v2 assertion', async () => { + const filePath = path.join(baseDir, 'trustnxt-icon.heic'); + const heicData = new Uint8Array(await fs.readFile(filePath)); + const asset = new BMFF(heicData); + const v2Assertion = new BMFFHashAssertion(2); + v2Assertion.algorithm = 'SHA-256' as HashAlgorithm; + v2Assertion.exclusions = [{ xpath: '/uuid' }]; + + const hash = await v2Assertion['hashBMFFWithExclusions'](asset); + assert.ok(hash instanceof Uint8Array); + assert.ok(hash.length > 0); + }); + + it('should correctly handle v3 merkle tree validation', async () => { + const mockAsset = new BMFF(createBMFFMock()); + const v3Assertion = new BMFFHashAssertion(3); + v3Assertion.algorithm = 'SHA-256' as HashAlgorithm; + + // Get the mdat box to find its correct offset + const mdatBox = mockAsset.getBoxByPath('/mdat'); + assert.ok(mdatBox, 'mdat box not found'); + + // Data starts after the box header (8 bytes) + const dataOffset = mdatBox.offset + 8; + + v3Assertion.merkle = [ + { + uniqueId: 1, + localId: 1, + count: 2, + hashes: [ + await Crypto.digest(await mockAsset.getDataRange(dataOffset, 4), 'SHA-256' as HashAlgorithm), + await Crypto.digest(await mockAsset.getDataRange(dataOffset + 4, 4), 'SHA-256' as HashAlgorithm), + ] as Uint8Array[], + fixedBlockSize: 4, + }, + ]; + + const result = await v3Assertion['validateMerkleTree'](mockAsset); + assert.ok(result.isValid); + }); + + it('should handle variable block sizes in merkle tree', async () => { + const mockAsset = new BMFF(createBMFFMock()); + assertion.algorithm = 'SHA-256' as HashAlgorithm; + + // Get the mdat box to find its correct offset + const mdatBox = mockAsset.getBoxByPath('/mdat'); + assert.ok(mdatBox, 'mdat box not found'); + + // Data starts after the box header (8 bytes) + const dataOffset = mdatBox.offset + 8; + + assertion.merkle = [ + { + uniqueId: 1, + localId: 1, + count: 2, + variableBlockSizes: [4, 4], + hashes: [ + await Crypto.digest(await mockAsset.getDataRange(dataOffset, 4), 'SHA-256' as HashAlgorithm), + await Crypto.digest(await mockAsset.getDataRange(dataOffset + 4, 4), 'SHA-256' as HashAlgorithm), + ] as Uint8Array[], + }, + ]; + + const result = await assertion['validateMerkleTree'](mockAsset); + assert.ok(result.isValid); + }); });