Skip to content

Commit

Permalink
C2PA v2.1 Updates - BMFF hash v3 assertion (#213)
Browse files Browse the repository at this point in the history
* Implement BMFF hash v3 assertion

* Enhance BMFF functionality with new box retrieval method and hash assertion improvements

* Refactor BMFFHashAssertion to support versioning and improve validation logic
Add unit tests

* remove unused code

* Add unit tests for v2 and v3 BMFF hash assertions

* add new v3 test cases, add a new asset type for 'trustnxt-icon-signed-v3.heic' in asset-reading tests.

* Refactor AssertionUtils and BMFFHashAssertion for improved exclusion 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.

* Implement HashAssertion interface in BMFFHashAssertion and DataHashAssertion classes

* bmff hash update logic

* Refactor BMFF hash assertion and box handling

* fix version

* resolve comments

* update readme

* update readme
  • Loading branch information
karlobencic authored Jan 28, 2025
1 parent eae180b commit 85916f6
Show file tree
Hide file tree
Showing 16 changed files with 935 additions and 133 deletions.
5 changes: 5 additions & 0 deletions .changeset/tiny-buses-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@trustnxt/c2pa-ts': minor
---

BMFF hash v3 assertion
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## About

`c2pa-ts` is a pure TypeScript implementation of [Coalition for Content Provenance and Authenticity (C2PA)](https://c2pa.org/) according to [specification version 2.0](https://c2pa.org/specifications/specifications/2.0/specs/C2PA_Specification.html).
`c2pa-ts` is a pure TypeScript implementation of [Coalition for Content Provenance and Authenticity (C2PA)](https://c2pa.org/) according to [specification version 2.1](https://c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html).

It does not use any native binaries or WebAssembly and is therefore truly platform independent. In modern browsers as well as Node.js it should run out of the box. In mobile apps or other environments lacking browser APIs, some external code may be necessary (see [below](#usage-in-constrained-environments) for details).

Expand All @@ -24,7 +24,7 @@ Anything that's not listed below is not currently planned to be implemented.
- :construction: Validating manifests (mostly implemented except chain of trust validation)
- :white_check_mark: Creating manifests

:information_source: On C2PA versions: The library is targeted at C2PA specification 2.0, however data structures from older versions of the specification are also supported for backwards compatibility.
:information_source: On C2PA versions: The library is targeted at C2PA specification 2.1, however data structures from older versions of the specification are also supported for backwards compatibility.

:information_source: Although it is a separate project from C2PA, the library also includes support for several [CAWG](https://github.com/creator-assertions/) assertions.

Expand All @@ -36,15 +36,16 @@ Anything that's not listed below is not currently planned to be implemented.
- :x: GIF
- :x: TIFF
- :x: WebP
- :x: JPEG XL

### Supported assertions

- :white_check_mark: Data Hash
- :white_check_mark: BMFF-Based Hash (except Merkle tree hashing)
- :white_check_mark: BMFF-Based Hash (v2 and v3)
- :x: General Boxes Hash
- :white_check_mark: Thumbnail
- :white_check_mark: Actions (except action templates and metadata)
- :white_check_mark: Ingredient
- :white_check_mark: Ingredient (v2 and v3)
- :white_check_mark: Metadata (specialized, common, generic, and CAWG variants)
- :white_check_mark: Creative Work
- :white_check_mark: Training and Data Mining (C2PA and CAWG variants)
Expand Down
12 changes: 6 additions & 6 deletions src/asset/BMFF.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { BaseAsset } from './BaseAsset';
import { Asset } from './types';

export class BMFF extends BaseAsset implements Asset {
public static c2paBoxUserType = [
0xd8, 0xfe, 0xc3, 0xd6, 0x1b, 0x0e, 0x48, 0x3c, 0x92, 0x97, 0x58, 0x28, 0x87, 0x7e, 0xc4, 0x81,
];

/** Currently supported major brand identifiers */
private static canReadBrands = new Set(['heic', 'mif1']);

Expand Down Expand Up @@ -234,7 +238,7 @@ class BoxReader {

// Now that the box header is fully read, we know that it might be a UUID box (== has a userType),
// so handle those cases as well (currently only C2PABox)
if (box.userType && BinaryHelper.bufEqual(box.userType, C2PABox.c2paUserType)) {
if (box.userType && BinaryHelper.bufEqual(box.userType, BMFF.c2paBoxUserType)) {
box = new C2PABox(pos, size, payloadPos, payloadSize, boxType);
box.readContents(buf);
}
Expand Down Expand Up @@ -578,10 +582,6 @@ interface C2PAManifestBoxPayload extends C2PABoxPayload {
}

class C2PABox extends FullBox<C2PABoxPayload> {
public static c2paUserType = [
0xd8, 0xfe, 0xc3, 0xd6, 0x1b, 0x0e, 0x48, 0x3c, 0x92, 0x97, 0x58, 0x28, 0x87, 0x7e, 0xc4, 0x81,
];

private static readonly headerLength =
4 + // size
4 + // type
Expand Down Expand Up @@ -630,7 +630,7 @@ class C2PABox extends FullBox<C2PABoxPayload> {
'uuid',
);

box.userType = new Uint8Array(C2PABox.c2paUserType);
box.userType = new Uint8Array(BMFF.c2paBoxUserType);
const payload: C2PAManifestBoxPayload = {
version: 0,
flags: 0,
Expand Down
4 changes: 3 additions & 1 deletion src/manifest/AssertionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ export class AssertionStore implements ManifestComponent {
if (label.label === AssertionLabels.actions || label.label === AssertionLabels.actionsV2) {
assertion = new ActionAssertion();
} else if (label.label === AssertionLabels.bmffV2Hash) {
assertion = new BMFFHashAssertion();
assertion = new BMFFHashAssertion(2);
} else if (label.label === AssertionLabels.bmffV3Hash) {
assertion = new BMFFHashAssertion(3);
} else if (label.label === AssertionLabels.creativeWork) {
assertion = new CreativeWorkAssertion();
} else if (label.label === AssertionLabels.dataHash) {
Expand Down
2 changes: 2 additions & 0 deletions src/manifest/assertions/AssertionLabels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ export class AssertionLabels {
public static readonly collectionHash = 'c2pa.hash.collection.data';
public static readonly bmffHash = 'c2pa.hash.bmff';
public static readonly bmffV2Hash = 'c2pa.hash.bmff.v2';
public static readonly bmffV3Hash = 'c2pa.hash.bmff.v3';
public static readonly hardBindings = [
AssertionLabels.dataHash,
AssertionLabels.boxHash,
AssertionLabels.collectionHash,
// "A validator or consumer shall not validate content authenticated by a c2pa.hash.bmff assertion. Instead, it shall report the content as unauthenticated, as if no manifest were present."
//AssertionLabels.bmffHash,
AssertionLabels.bmffV2Hash,
AssertionLabels.bmffV3Hash,
];

public static readonly ingredient = 'c2pa.ingredient';
Expand Down
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
Loading

0 comments on commit 85916f6

Please sign in to comment.