c2pa-fonts
v0.3.0
Published
C2PA manifest embedding, hard binding, and verification for OpenType/TrueType (SFNT) fonts
Maintainers
Readme
Overview
Implements the font embedding method from the C2PA Technical Specification and its c2pa.hash.data hard binding, for fonts that conform to the OpenType or OFF (ISO/IEC 14496-22) specification.
The specification marks this table format as preliminary: "The
C2PAtable format is not yet defined in the OFF nor OTF specification; the following definition is preliminary." The layout below tracks that definition and will follow it if it changes.
The manifest is stored in a dedicated SFNT table with the tag C2PA, which may carry an embedded Manifest Store, a remote manifest URI, or both:
| Field | Type | Description |
|---|---|---|
| majorVersion / minorVersion | uint16 | Version of the C2PA font table |
| activeManifestUri | Offset32 + uint16 | URI of the active manifest (offset + length; 0 when absent) |
| manifestStore | Offset32 + uint32 | Embedded C2PA Manifest Store (offset + length; 0 when absent) |
The
C2PAfont table is preliminary. The C2PA specification states the table format "is not yet defined in the OFF nor OpenType specification; the following definition is preliminary." There is no stable, ratified conformance requirement for fonts. This crate tracks the preliminary definition and emits table version0.1.
What this crate does — and does not
Validating a font's provenance is a fixed pipeline. Only two steps are font-specific; this crate owns exactly those:
| Step | Owner |
|---|---|
| 1. Locate + extract the Manifest Store | c2pa-fonts |
| 2. Parse the JUMBF/CBOR manifest | c2pa-rs |
| 3. Verify the COSE signature | c2pa-rs |
| 4. Evaluate the X.509 trust chain | c2pa-rs |
| 5. Hard binding: c2pa.hash.data exclusion geometry over the font | c2pa-fonts |
| 6. Validate assertions / ingredients | c2pa-rs |
This crate does not build manifests, sign, or implement COSE/trust — that is the official c2pa SDK's job. With the validation feature it delegates steps 2–4 and 6 to c2pa-rs, so an application using both can act as a C2PA generator and verifier for fonts. (This crate is a building block; C2PA conformance certification is a separate program for products, which this crate makes no claim to.)
Font collections (.ttc/ttcf) and WOFF/WOFF2 are rejected explicitly; decompress WOFF to SFNT first.
Quick Start
[dependencies]
c2pa-fonts = "0.2"
# Hard-binding hashers and the c2pa-rs validation bridge:
c2pa-fonts = { version = "0.2", features = ["validation"] }Bindings for the zero-dependency core (embed, extract, hard-binding geometry) ship for JavaScript and Python:
npm install @writerslogic/c2pa-fonts # wasm-bindgen, camelCase API
pip install c2pa-fonts # PyO3 abi3 wheelEmbed an already-signed manifest
use c2pa_fonts::{embed_manifest, ManifestSource};
let font: &[u8] = /* .ttf / .otf bytes */;
let signed = embed_manifest(font, ManifestSource::embedded(manifest_store)).unwrap();
// or ManifestSource::remote("https://example.com/m.c2pa") / ManifestSource::both(uri, store)Generate with a hard binding (placeholder-then-fill)
The manifest signs over the font, so the font must be laid out before signing. Reserve the store, hash over the returned exclusions, sign a manifest that fits, then fill:
use c2pa_fonts::{reserve_manifest, fill_manifest, data_hash_ranges};
// 1. Reserve space; get the font-with-placeholder and its exclusions.
let reserved = reserve_manifest(font, 30_000, None).unwrap();
// 2. Hash reserved.font over reserved.exclusions and sign a data-hashed manifest
// with the c2pa SDK (see tests/roundtrip.rs for the full flow).
// data_hash_ranges(&reserved.font) yields the ranges as c2pa `HashRange`s.
// 3. Fill the reserved region with the signed manifest (<= reserved size).
let final_font = fill_manifest(&reserved.font, &signed_manifest).unwrap();The exclusions cover the manifest store and the two checksum fields that depend on it (the C2PA table's directory checksum and head.checkSumAdjustment), so filling never invalidates the hash.
Read and verify
use c2pa_fonts::{read_manifest, read_manifest_uri, verify, validate};
let manifest = read_manifest(&final_font).unwrap(); // embedded store bytes
let uri = read_manifest_uri(&final_font).unwrap(); // Option<String>
let report = verify(&final_font).unwrap(); // structural, zero-dep
assert!(report.is_compliant());
// End-to-end (feature = "validation"): hard binding here, signature/trust via c2pa-rs.
let result = validate(&final_font).unwrap();
assert!(result.hard_binding_valid);
// result.state, result.success_codes, result.failure_codes, result.manifest_jsonDesign
- The Manifest Store and/or active manifest URI live in a single
C2PASFNT table - Embedding normalizes the font: the table directory is re-sorted by tag, offsets re-aligned to 4-byte boundaries, and per-table checksums plus
head.checkSumAdjustmentrecomputed verifychecksC2PAtable well-formedness and SFNT checksum integrity; more than oneC2PAtable is rejectedvalidate(feature) extracts the store and delegates COSE/trust/assertion validation to c2pa-rs; trust uses c2pa-rs default settings — inspect the returned state and status codes- Single fonts only; collections and WOFF are rejected
Related Crates
Part of a family of single-purpose crates, one per C2PA embedding method. Each is standalone and independently versioned.
| Crate | Description |
|---|---|
| c2pa-structured-text | Structured text: ASCII-armoured manifest in a comment or front matter |
| c2pa-unstructured-text | Unstructured text: invisible Unicode variation-selector run |
| c2pa-html | HTML: script and link elements in the document head |
| c2pa-http | HTTP: the c2pa-manifest Link header, with a Tower middleware |
| c2pa-text-binding | Soft binding and content fingerprinting for text assets |
| c2pa-vtt | WebVTT caption and subtitle embedding |
| c2pa-zip | ZIP-based documents: EPUB, DOCX, ODT, OXPS |
| c2pa-warc | WARC web archive embedding (ISO 28500) |
| c2pa-ml | ML model containers: GGUF, SafeTensors, ONNX |
| c2pa | Official C2PA SDK |
Security
Found a vulnerability? Please report it privately — see SECURITY.md.
License
Licensed under either of Apache License, Version 2.0 or MIT License at your option.
Built by WritersLogic
