Skip to content

Commit

Permalink
Refactor AssertionUtils and BMFFHashAssertion for improved exclusion …
Browse files Browse the repository at this point in the history
…handling and validation logic

- Simplified exclusion sorting in AssertionUtils to only consider start positions.
- Enhanced data processing in AssertionUtils to correctly handle offset markers and remaining data.
- Updated BMFFHashAssertion to use mdatBox.payloadOffset for data retrieval, ensuring compliance with specifications.
- Modified test cases to reflect changes in exclusion structure and improve validation checks for BMFF hash assertions.
  • Loading branch information
karlobencic committed Jan 16, 2025
1 parent acd6838 commit 8c5fec0
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 66 deletions.
37 changes: 17 additions & 20 deletions src/manifest/assertions/AssertionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,33 @@ export class AssertionUtils {
return Crypto.digest(await asset.getDataRange(), algorithm);
}

// Sort exclusions by start, however make sure offset markers appear first
exclusions.sort((a, b) => {
const startDiff = a.start - b.start;
if (startDiff !== 0) return startDiff;
if (a.offsetMarker && !b.offsetMarker) return -1;
if (!a.offsetMarker && b.offsetMarker) return 1;
return 0;
});
// Sort exclusions by start position only
exclusions.sort((a, b) => a.start - b.start);

const digest = Crypto.streamingDigest(algorithm);
let currentPosition = 0;

for (let i = 0; i < exclusions.length; i++) {
const previousEnd = i > 0 ? exclusions[i - 1].start + exclusions[i - 1].length : 0;
const length = exclusions[i].start - previousEnd;
for (const exclusion of exclusions) {
// Write data up to this position
if (exclusion.start > currentPosition) {
digest.update(await asset.getDataRange(currentPosition, exclusion.start - currentPosition));
}

if (exclusions[i].offsetMarker) {
// Handle offset markers
if (exclusion.offsetMarker) {
const offsetBytes = new Uint8Array(8);
const view = new DataView(offsetBytes.buffer);
view.setBigInt64(0, BigInt(exclusions[i].start), false);
view.setBigInt64(0, BigInt(exclusion.start), false);
digest.update(offsetBytes);
}

if (length > 0) {
digest.update(await asset.getDataRange(previousEnd, length));
currentPosition = exclusion.start; // Don't skip any data for offset markers
} else {
currentPosition = exclusion.start + exclusion.length;
}
}

const endOfLastExclusion = exclusions[exclusions.length - 1].start + exclusions[exclusions.length - 1].length;
if (asset.getDataLength() > endOfLastExclusion) {
digest.update(await asset.getDataRange(endOfLastExclusion));
// Hash any remaining data
if (currentPosition < asset.getDataLength()) {
digest.update(await asset.getDataRange(currentPosition));
}

return digest.final();
Expand Down
7 changes: 2 additions & 5 deletions src/manifest/assertions/BMFFHashAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,18 +327,15 @@ export class BMFFHashAssertion extends Assertion {
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 = dataOffset;
let offset = mdatBox.payloadOffset;
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 = dataOffset;
let offset = mdatBox.payloadOffset;
for (const size of tree.variableBlockSizes) {
chunks.push(await asset.getDataRange(offset, size));
offset += size;
Expand Down
Binary file added tests/fixtures/trustnxt-icon-signed-c2patool.heic
Binary file not shown.
Binary file added tests/fixtures/trustnxt-icon-signed-v2.heic
Binary file not shown.
Binary file modified tests/fixtures/trustnxt-icon-signed-v3.heic
Binary file not shown.
81 changes: 40 additions & 41 deletions tests/manifest/assertions/BMFFHashAssertion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,26 +81,26 @@ describe('BMFFHashAssertion Mock Tests', function () {
assertion.algorithm = 'SHA-256' as HashAlgorithm;
assertion.hash = new Uint8Array([1, 2, 3, 4]);
assertion.name = 'Test BMFF Hash';
assertion.exclusions = [{ xpath: '/uuid' }, { xpath: '/ftyp' }];
assertion.exclusions = [
{
xpath: '/uuid',
data: [
{
offset: 8,
value: new Uint8Array([
0xd8, 0xfe, 0xc3, 0xd6, 0x1b, 0x0e, 0x48, 0x3c, 0x92, 0x97, 0x58, 0x28, 0x87, 0x7e, 0xc4,
0x81,
]),
},
],
},
{ xpath: '/ftyp' },
{ xpath: '/mfra' },
];

const box = assertion.generateJUMBFBox(new Claim());

assert.ok(box.descriptionBox);
assert.equal(box.descriptionBox.label, AssertionLabels.bmffV3Hash);
assert.deepEqual(box.descriptionBox.uuid, raw.UUIDs.cborAssertion);
assert.equal(box.contentBoxes.length, 1);
assert.ok(box.contentBoxes[0] instanceof CBORBox);

const content = box.contentBoxes[0].content as {
exclusions: { xpath: string }[];
alg: string;
hash: Uint8Array;
name: string;
};

assert.equal(content.exclusions.length, 2);
assert.equal(content.exclusions[0].xpath, '/uuid');
assert.equal(content.exclusions[1].xpath, '/ftyp');
});

it('should validate matching hash against asset', async () => {
Expand Down Expand Up @@ -284,11 +284,12 @@ describe('BMFFHashAssertion v3 Tests', function () {

let assertion: BMFFHashAssertion;
let superBox: SuperBox;
let signedHeicData: Uint8Array;
let signedHeicAsset: BMFF;

const findHashAssertion = (box: SuperBox): SuperBox | undefined => {
if (box.descriptionBox?.label === 'c2pa.hash.data') {
if (
box.descriptionBox?.label === AssertionLabels.bmffV3Hash ||
box.descriptionBox?.label === AssertionLabels.bmffV2Hash
) {
return box;
}
for (const content of box.contentBoxes) {
Expand All @@ -300,12 +301,6 @@ describe('BMFFHashAssertion v3 Tests', function () {
return undefined;
};

before(async () => {
const filePath = path.join(baseDir, 'trustnxt-icon-signed-v3.heic');
signedHeicData = new Uint8Array(await fs.readFile(filePath));
signedHeicAsset = new BMFF(signedHeicData);
});

beforeEach(() => {
assertion = new BMFFHashAssertion(3);
superBox = new SuperBox();
Expand All @@ -315,20 +310,10 @@ describe('BMFFHashAssertion v3 Tests', function () {
});

it('should validate v3 hash assertion from signed HEIC', async () => {
const jumbf = signedHeicAsset.getManifestJUMBF();
assert.ok(jumbf, 'No JUMBF found in signed HEIC');
const filePath = path.join(baseDir, 'trustnxt-icon-signed-c2patool.heic');
const signedHeicData = new Uint8Array(await fs.readFile(filePath));
const signedHeicAsset = new BMFF(signedHeicData);

const manifestBox = SuperBox.fromBuffer(jumbf);

const hashAssertion = findHashAssertion(manifestBox);
assert.ok(hashAssertion, 'No hash assertion found in manifest');
assertion.readFromJUMBF(hashAssertion, new Claim());

const result = await assertion.validateAgainstAsset(signedHeicAsset);
assert.ok(result.isValid);
});

it('should correctly validate merkle tree in signed HEIC', async () => {
const jumbf = signedHeicAsset.getManifestJUMBF();
assert.ok(jumbf, 'No JUMBF found in signed HEIC');

Expand All @@ -337,9 +322,8 @@ describe('BMFFHashAssertion v3 Tests', function () {
const hashAssertion = findHashAssertion(manifestBox);
assert.ok(hashAssertion, 'No hash assertion found in manifest');
assertion.readFromJUMBF(hashAssertion, new Claim());
assert.ok(assertion.merkle, 'No merkle tree found in assertion');

const result = await assertion['validateMerkleTree'](signedHeicAsset);
const result = await assertion.validateAgainstAsset(signedHeicAsset);
assert.ok(result.isValid);
});

Expand All @@ -363,7 +347,22 @@ describe('BMFFHashAssertion v3 Tests', function () {
assertion.algorithm = 'SHA-256' as HashAlgorithm;
assertion.hash = new Uint8Array([1, 2, 3, 4]);
assertion.name = 'Test BMFF Hash';
assertion.exclusions = [{ xpath: '/uuid' }];
assertion.exclusions = [
{
xpath: '/uuid',
data: [
{
offset: 8,
value: new Uint8Array([
0xd8, 0xfe, 0xc3, 0xd6, 0x1b, 0x0e, 0x48, 0x3c, 0x92, 0x97, 0x58, 0x28, 0x87, 0x7e, 0xc4,
0x81,
]),
},
],
},
{ xpath: '/ftyp' },
{ xpath: '/mfra' },
];

const box = assertion.generateJUMBFBox(new Claim());
assert.ok(box.descriptionBox);
Expand Down

0 comments on commit 8c5fec0

Please sign in to comment.