diff --git a/docs/context.md b/docs/context.md new file mode 100644 index 00000000..fa36c615 --- /dev/null +++ b/docs/context.md @@ -0,0 +1,686 @@ +# Using Context to configure the SDK + +Use the `Context` class to configure how `Reader`, `Builder`, and other aspects of the SDK operate. + +## What is Context? + +Context encapsulates SDK configuration: + +- **Settings**: Verification options, [Builder behavior](#configuring-builder), [Reader trust configuration](#configuring-reader), thumbnail configuration, and more. See [Using settings](settings.md) for complete details. +- [**Signer configuration**](#configuring-a-signer): Optional signer credentials that can be stored in the Context for reuse. +- **State isolation**: Each `Context` is independent, allowing different configurations to coexist in the same application. + +### Why use Context? + +`Context` is better than the deprecated global `load_settings()` function because it: + +- **Makes dependencies explicit**: Configuration is passed directly to `Reader` and `Builder`, not hidden in global state. +- **Enables multiple configurations**: Run different configurations simultaneously. For example, one for development with test certificates, another for production with strict validation. +- **Eliminates global state**: Each `Reader` and `Builder` gets its configuration from the `Context` you pass, avoiding subtle bugs from shared state. +- **Simplifies testing**: Create isolated configurations for tests without worrying about cleanup or interference between them. +- **Improves code clarity**: Reading `Builder(manifest_json, ctx)` immediately shows that configuration is being used. + +### Class diagram + +This diagram shows the public classes in the SDK and their relationships. + +```mermaid +classDiagram + direction LR + + class Settings { + +from_json(json_str) Settings$ + +from_dict(config) Settings$ + +set(path, value) Settings + +update(data) Settings + +close() + +is_valid bool + } + + class ContextProvider { + <> + +is_valid bool* + +execution_context* + } + + class Context { + +from_json(json_str, signer) Context$ + +from_dict(config, signer) Context$ + +has_signer bool + +is_valid bool + +close() + } + + class Reader { + +get_supported_mime_types() list~str~$ + +try_create(format_or_path, stream, manifest_data, context) Reader | None$ + +json() str + +detailed_json() str + +get_active_manifest() dict | None + +get_manifest(label) dict + +get_validation_state() str | None + +get_validation_results() dict | None + +resource_to_stream(uri, stream) int + +is_embedded() bool + +get_remote_url() str | None + +close() + } + + class Builder { + +from_json(manifest_json, context) Builder$ + +from_archive(stream) Builder$ + +get_supported_mime_types() list~str~$ + +set_no_embed() + +set_remote_url(url) + +set_intent(intent, digital_source_type) + +add_resource(uri, stream) + +add_ingredient(json, format, source) + +add_action(action_json) + +to_archive(stream) + +with_archive(stream) Builder + +sign(signer, format, source, dest) bytes + +sign(format, source, dest) bytes + +sign_file(source_path, dest_path, signer) bytes + +close() + } + + class Signer { + +from_info(signer_info) Signer$ + +from_callback(callback, alg, certs, tsa_url) Signer$ + +reserve_size() int + +close() + } + + class C2paSignerInfo { + <> + +alg + +sign_cert + +private_key + +ta_url + } + + class C2paSigningAlg { + <> + ES256 + ES384 + ES512 + PS256 + PS384 + PS512 + ED25519 + } + + class C2paBuilderIntent { + <> + CREATE + EDIT + UPDATE + } + + class C2paDigitalSourceType { + <> + DIGITAL_CAPTURE + DIGITAL_CREATION + TRAINED_ALGORITHMIC_MEDIA + ... + } + + class C2paError { + <> + +message str + } + + class C2paError_Subtypes { + <> + ManifestNotFound + NotSupported + Json + Io + Verify + Signature + ... + } + + ContextProvider <|-- Context : extends + Settings --> Context : optional input + Signer --> Context : optional, consumed + C2paSignerInfo --> Signer : creates via from_info + C2paSigningAlg --> C2paSignerInfo : alg field + C2paSigningAlg --> Signer : from_callback alg + Context --> Reader : context= + Context --> Builder : context= + Signer --> Builder : sign(signer) + C2paBuilderIntent --> Builder : set_intent + C2paDigitalSourceType --> Builder : set_intent + C2paError --> C2paError_Subtypes : subclasses +``` + +> [!NOTE] +> The deprecated `load_settings()` function still works for backward compatibility but you are encouraged to migrate your code to use `Context`. See [Migrating from load_settings](#migrating-from-load_settings). + +## Workflow overview + +The SDK supports two main workflows. `Settings` and `Context` are currently optional in both (but recommended). `Reader` and `Builder` can still be used directly with SDK defaults. + +### Reading provenance + +Read and inspect C2PA data already embedded in (or attached to) an asset: + +```mermaid +flowchart LR + A[Asset file] --> B["Reader (with Context containing Settings)"] + B --> C["Manifest JSON (reader.json())"] + B --> D["Binary resources (reader.resource_to_stream())"] +``` + +```py +from c2pa import Reader + +reader = Reader("signed_image.jpg") +print(reader.json()) # Manifest store as JSON +``` + +### Signing content + +Create new C2PA provenance data and sign it into an asset: + +```mermaid +flowchart LR + A["Settings"] --> B["Context"] + F[Signer] --> B + B --> C[Builder] + G["add assertions
add ingredients"] --> C + C --> D["sign()"] + D --> E[Signed asset] + F2[Signer] -.-> D +``` + +```py +from c2pa import Builder, Signer, C2paSignerInfo, C2paSigningAlg + +builder = Builder(manifest_json) +# ... add assertions, ingredients, resources ... +with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +`Settings` and `Context` enable to customize behavior (trust configuration, thumbnail settings, claim generator info, and so on). + +## Creating a Context + +There are several ways to create a `Context`, depending on your needs: + +- [Using SDK default settings](#using-sdk-default-settings) +- [From a JSON string](#from-a-json-string) +- [From a dictionary](#from-a-dictionary) +- [From a Settings object](#from-a-settings-object) + +### Using SDK default settings + +Without additional parameters, a default context is using [SDK default settings](settings.md#default-configuration). + +**When to use:** For quick prototyping, or when you're happy with SDK default behavior (verification enabled, thumbnails enabled at 1024px, and so on). + +```py +from c2pa import Context + +ctx = Context() # Uses SDK defaults +``` + +### From a JSON string + +You can create a `Context` directly from a JSON configuration string. + +**When to use:** For simple configuration that doesn't need to be shared across the codebase, or when hard-coding settings for a specific purpose (for example, a utility script). + +```py +ctx = Context.from_json('''{ + "verify": {"verify_after_sign": true}, + "builder": { + "thumbnail": {"enabled": false}, + "claim_generator_info": {"name": "An app", "version": "0.1.0"} + } +}''') +``` + +### From a dictionary + +You can create a `Context` from a Python dictionary. + +**When to use:** When you want to build configuration programmatically using native Python data structures. + +```py +ctx = Context.from_dict({ + "verify": {"verify_after_sign": True}, + "builder": { + "thumbnail": {"enabled": False}, + "claim_generator_info": {"name": "An app", "version": "0.1.0"} + } +}) +``` + +### From a Settings object + +You can build a `Settings` object programmatically, then create a `Context` from that. + +**When to use:** For configuration that needs runtime logic (such as conditional settings based on environment), or when you want to build settings incrementally. + +```py +from c2pa import Settings, Context + +settings = Settings() +settings.set("builder.thumbnail.enabled", "false") +settings.set("verify.verify_after_sign", "true") +settings.update({ + "builder": { + "claim_generator_info": {"name": "An app", "version": "0.1.0"} + } +}) + +ctx = Context(settings) +``` + +## Common configuration patterns + +### Development environment with test certificates + +During development, you often need to trust self-signed or custom CA certificates: + +```py +# Load your test root CA +with open("test-ca.pem", "r") as f: + test_ca = f.read() + +ctx = Context.from_dict({ + "trust": { + "user_anchors": test_ca + }, + "verify": { + "verify_after_reading": True, + "verify_after_sign": True, + "remote_manifest_fetch": False, + "ocsp_fetch": False + }, + "builder": { + "claim_generator_info": {"name": "Dev Build", "version": "dev"}, + "thumbnail": {"enabled": False} + } +}) +``` + +### Configuration from environment variables + +Adapt configuration based on the runtime environment: + +```py +import os + +env = os.environ.get("ENVIRONMENT", "dev") + +settings = Settings() +if env == "production": + settings.update({"verify": {"strict_v1_validation": True}}) +else: + settings.update({"verify": {"remote_manifest_fetch": False}}) + +ctx = Context(settings) +``` + +### Layered configuration + +Load base configuration and apply runtime overrides: + +```py +import json + +# Load base configuration from a file +with open("config/base.json", "r") as f: + base_config = json.load(f) + +settings = Settings.from_dict(base_config) + +# Apply environment-specific overrides +settings.update({"builder": {"claim_generator_info": {"version": app_version}}}) + +ctx = Context(settings) +``` + +For the full list of settings and defaults, see [Using settings](settings.md). + +## Configuring Reader + +Use `Context` to control how `Reader` validates manifests and handles remote resources, including: + +- **Verification behavior**: Whether to verify after reading, check trust, and so on. +- [**Trust configuration**](#trust-configuration): Which certificates to trust when validating signatures. +- [**Network access**](#offline-operation): Whether to fetch remote manifests or OCSP responses. + +> [!IMPORTANT] +> `Context` is used only at construction. `Reader` copies the configuration it needs internally, so the `Context` object does not need to outlive the `Reader`. A `Context` object can also be reused for multiple `Reader` object instances. + +```py +ctx = Context.from_dict({"verify": {"remote_manifest_fetch": False}}) +reader = Reader("image.jpg", context=ctx) +``` + +### Reading from a file + +```py +ctx = Context.from_dict({ + "verify": { + "remote_manifest_fetch": False, + "ocsp_fetch": False + } +}) + +reader = Reader("image.jpg", context=ctx) +print(reader.json()) +``` + +### Reading from a stream + +```py +with open("image.jpg", "rb") as stream: + reader = Reader("image/jpeg", stream, context=ctx) + print(reader.json()) +``` + +### Trust configuration + +Example of trust configuration in a settings dictionary: + +```py +ctx = Context.from_dict({ + "trust": { + "user_anchors": "-----BEGIN CERTIFICATE-----\nMIICEzCCA...\n-----END CERTIFICATE-----", + "trust_config": "1.3.6.1.4.1.311.76.59.1.9\n1.3.6.1.4.1.62558.2.1" + } +}) + +reader = Reader("signed_asset.jpg", context=ctx) +``` + +### Full validation + +To configure full validation, with all verification features enabled: + +```py +ctx = Context.from_dict({ + "verify": { + "verify_after_reading": True, + "verify_trust": True, + "verify_timestamp_trust": True, + "remote_manifest_fetch": True + } +}) + +reader = Reader("asset.jpg", context=ctx) +``` + +For more information, see [Settings - Verify](settings.md#verify). + +### Offline operation + +To configure `Reader` to work with no network access: + +```py +ctx = Context.from_dict({ + "verify": { + "remote_manifest_fetch": False, + "ocsp_fetch": False + } +}) + +reader = Reader("local_asset.jpg", context=ctx) +``` + +For more information, see [Settings - Offline or air-gapped environments](settings.md#offline-or-air-gapped-environments). + +## Configuring Builder + +`Builder` uses `Context` to control how to create and sign C2PA manifests. The `Context` affects: + +- **Claim generator information**: Application name, version, and metadata embedded in the manifest. +- **Thumbnail generation**: Whether to create thumbnails, size, quality, and format. +- **Action tracking**: Auto-generation of actions like `c2pa.created`, `c2pa.opened`, `c2pa.placed`. +- **Intent**: The purpose of the claim (create, edit, or update). +- **Verification after signing**: Whether to validate the manifest immediately after signing. +- **Signer configuration** (optional): Credentials can be stored in the context for reuse. + +> [!IMPORTANT] +> The `Context` is used only when constructing the `Builder`. The `Builder` copies the configuration it needs internally, so the `Context` object does not need to outlive the `Builder`. A `Context` object can also be reused for multiple `Builder` object instances. + +### Context and archives + +Archives (`.c2pa` files) store only the manifest definition. They do **not** store settings or context. This means: + +- **`Builder.from_archive(stream)`** creates a context-free builder. All settings revert to SDK defaults regardless of what context the original builder had. +- **`Builder({}, ctx).with_archive(stream)`** creates a builder with a context first, then loads the archived manifest definition into it. The context settings are preserved and propagated to this Builder instance. + +Use `with_archive()` when your workflow depends on specific settings (thumbnails, claim generator, intent, and so on). Use `from_archive()` only for quick prototyping where SDK defaults are acceptable. + +```py +# Recommended: with_archive propagates context settings +ctx = Context.from_dict({ + "builder": { + "thumbnail": {"enabled": False}, + "claim_generator_info": {"name": "My App", "version": "0.1.0"} + } +}) + +with open("manifest.c2pa", "rb") as archive: + builder = Builder({}, ctx) + builder.with_archive(archive) + # builder now has the archived definition + context settings +``` + +For more details on archive workflows, see [Working with archives](working-stores.md#working-with-archives). + +### Basic use + +```py +ctx = Context.from_dict({ + "builder": { + "claim_generator_info": { + "name": "An app", + "version": "0.1.0" + }, + "intent": {"Create": "digitalCapture"} + } +}) + +builder = Builder(manifest_json, ctx) + +# Pass signer explicitly at signing time +with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +### Controlling thumbnail generation + +```py +# Disable thumbnails for faster processing +no_thumbnails_ctx = Context.from_dict({ + "builder": { + "claim_generator_info": {"name": "Batch Processor"}, + "thumbnail": {"enabled": False} + } +}) + +# Or customize thumbnail size and quality e.g. for mobile +mobile_ctx = Context.from_dict({ + "builder": { + "claim_generator_info": {"name": "Mobile App"}, + "thumbnail": { + "enabled": True, + "long_edge": 512, + "quality": "low", + "prefer_smallest_format": True + } + } +}) +``` + +## Configuring a Signer + +### Signing concepts + +C2PA uses a certificate-based trust model to prove who signed an asset. When creating a `Signer`, the following parameters are required: + +- **Certificate chain** (`sign_cert`): An X.509 certificate chain in PEM format. The first certificate identifies the signer; subsequent certificates form a chain up to a trusted root (trust anchor). Verifiers use this chain to confirm that the signature comes from a trusted source. +- **Timestamp authority URL** (`ta_url`): An optional [RFC 3161](https://www.rfc-editor.org/rfc/rfc3161) timestamp server URL. When provided, the SDK requests a trusted timestamp during signing. This proves _when_ the signature was made. Timestamping matters because signatures remain verifiable even after the signing certificate expires, as long as the certificate was valid at the time of signing. + +### Signer creation patterns + +A Signer can be configured two ways: + +- [From Settings (signer-on-context)](#from-settings): pass the signer when creating the `Context`. +- [Explicit signer passed to sign()](#explicit-programmatic-signer): pass the signer directly at signing time. + +### From Settings + +Create a `Signer` and pass it to the `Context`. The signer is **consumed**: the `Signer` object becomes invalid after this call and must not be reused directly after that point. The `Context` takes ownership of the underlying native signer. + +```py +from c2pa import Context, Settings, Builder, Signer, C2paSignerInfo, C2paSigningAlg + +# Create a signer +signer_info = C2paSignerInfo( + C2paSigningAlg.ES256, cert_data, key_data, b"http://timestamp.digicert.com" +) +signer = Signer.from_info(signer_info) + +# Create context with signer (signer is consumed) +ctx = Context(settings, signer) +# signer is now invalid and must not be used directly again + +# Build and sign, no signer argument needed since a Signer is in the Context +builder = Builder(manifest_json, ctx) +with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: + builder.sign("image/jpeg", src, dst) +``` + +### Explicit (programmatic) signer + +For full programmatic control, create a `Signer` and pass it directly to `Builder.sign()`: + +```py +signer = Signer.from_info(signer_info) +builder = Builder(manifest_json, ctx) + +with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: + builder.sign(signer, "image/jpeg", src, dst) +``` + +You can also use the fluent `ContextBuilder` API to attach a signer programmatically via `with_signer`: + +```py +ctx = Context.builder().with_settings(settings).with_signer(signer).build() +``` + +### Precedence rules for Signer configuration + +If both an explicit signer and a context signer are available, the explicit signer always takes precedence: + +```py +# Explicit signer wins over context signer +builder.sign(explicit_signer, "image/jpeg", source, dest) +``` + +## Context lifetime and usage + +### `with` statement + +`Context` supports the `with` statement for automatic resource cleanup: + +```py +with Context() as ctx: + reader = Reader("image.jpg", context=ctx) + print(reader.json()) +# Resources are automatically released +``` + +### Reusable contexts + +You can reuse the same `Context` to create multiple readers and builders: + +```py +ctx = Context(settings) + +# All three use the same configuration through usage of the same context +builder1 = Builder(manifest1, ctx) +builder2 = Builder(manifest2, ctx) +reader = Reader("image.jpg", context=ctx) + +# Context can be closed after construction; readers/builders still work +``` + +Using the `with` statement for automatic cleanup: + +```py +with Context(settings) as ctx: + builder1 = Builder(manifest1, ctx) + builder2 = Builder(manifest2, ctx) + reader = Reader("image.jpg", context=ctx) +# Resources are automatically released +``` + +### Multiple contexts for different purposes + +Use different `Context` objects when you need different settings. Ror example, for development vs. production, or different trust configurations: + +```py +dev_ctx = Context(dev_settings) +prod_ctx = Context(prod_settings) + +# Different builders with different configurations +dev_builder = Builder(manifest, dev_ctx) +prod_builder = Builder(manifest, prod_ctx) +``` + +### ContextProvider abstract base class + +`ContextProvider` is an abstract base class (ABC) that enables context provider implementations. Subclass it and implement the `is_valid` and `execution_context` abstract properties to create a provider that can be passed to `Reader` or `Builder` as `Context`. + +```py +from c2pa import ContextProvider, Context + +# The built-in Context inherits from ContextProvider +ctx = Context() +assert isinstance(ctx, ContextProvider) # True +``` + +## Migrating from load_settings + +The `load_settings()` function is deprecated. Replace it with `Settings` and `Context` APIs instead: + +| Aspect | load_settings (legacy) | Context | +|--------|------------------------|---------| +| Scope | Global state | Per Reader/Builder, passed explicitly | +| Multiple configs | Not supported | One context per configuration | +| Testing | Shared global state | Isolated contexts per test | + +**Deprecated:** + +```py +from c2pa import load_settings, Reader + +load_settings({"builder": {"thumbnail": {"enabled": False}}}) +reader = Reader("image.jpg") # uses global settings +``` + +**Using current APIs:** + +```py +from c2pa import Settings, Context, Reader + +settings = Settings.from_dict({"builder": {"thumbnail": {"enabled": False}}}) +ctx = Context(settings) +reader = Reader("image.jpg", context=ctx) +``` + +## See also + +- [Using settings](settings.md): schema, property reference, and examples. +- [Usage](usage.md): reading and signing with Reader and Builder. +- [CAI settings schema](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/): full schema reference. diff --git a/docs/intents.md b/docs/intents.md new file mode 100644 index 00000000..e227a792 --- /dev/null +++ b/docs/intents.md @@ -0,0 +1,455 @@ +# Using Builder intents + +Intents enable validation, add required default actions, and help prevent invalid operations when using a `Builder`. Intents are about the operation (create, edit, update) executed on the source asset. + +## Why use intents? + +Without intents, the caller must manually construct the correct manifest structure: adding the required actions (`c2pa.created` or `c2pa.opened` as the first action per the specification), setting digital source types, managing ingredients, and linking actions to ingredients. Getting any of this wrong produces a non-compliant manifest. + +With intents, the caller declares *what is being done* and the Builder handles the rest: + +```py +# Without intents: a caller must manually wire things up, and make sure ingredients are properly linked to actions. +# This is especially important in the case of parentOf ingredient relationships, with the c2pa.opened action +with Builder({ + "assertions": [ + { + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", + } + ] + }, + } + ], +}) as builder: + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) + +# With intents: the Builder generates the actions automatically +with Builder({}) as builder: + builder.set_intent( + C2paBuilderIntent.CREATE, + C2paDigitalSourceType.TRAINED_ALGORITHMIC_MEDIA, + ) + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +Both ways of writing the code produce the same signed manifest. With intents, the Builder validates the setup and fills in the spec-required structure. + +## Setting the intent + +There are three ways to set the intent on a `Builder` object instance. + +### Using Context + +Pass the intent through a `Context` object when creating the `Builder`. This keeps intent configuration alongside other builder settings such as `claim_generator_info` and `thumbnail`. + +```py +from c2pa import Context, Builder + +ctx = Context.from_dict({ + "builder": { + "intent": {"Create": "digitalCapture"}, + "claim_generator_info": {"name": "My App", "version": "0.1.0"}, + } +}) + +with Builder({}, context=ctx) as builder: + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +The same `Context` can be reused across multiple `Builder` instances, ensuring consistent configuration: + +```py +ctx = Context.from_dict({ + "builder": { + "intent": "edit", + "claim_generator_info": {"name": "Batch Editor"}, + } +}) + +for path in image_paths: + with Builder({}, context=ctx) as builder: + builder.sign_file(path, output_path(path), signer) +``` + +### Using `set_intent` on the Builder + +Call `set_intent` directly on a `Builder` instance. This is useful for one-off operations or when the intent needs to be determined at runtime: + +```py +with Builder({}) as builder: + builder.set_intent( + C2paBuilderIntent.CREATE, + C2paDigitalSourceType.TRAINED_ALGORITHMIC_MEDIA, + ) + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +### Intent setting precedence + +When an intent is configured in multiple places , the most specific setting wins: + +```mermaid +flowchart TD + Check{Was set_intent called + on the Builder?} + Check --> |Yes| UseSetIntent["Use set_intent value"] + Check --> |No| CheckCtx{Was a Context with + builder.intent provided?} + CheckCtx --> |Yes| UseCtx["Use Context intent"] + CheckCtx --> |No| CheckGlobal{Was load_settings called + with builder.intent?} + CheckGlobal --> |Yes| UseGlobal["Use global intent + (deprecated)"] + CheckGlobal --> |No| NoIntent["No intent set. + Caller must define actions + manually in manifest JSON."] +``` + +If a `set_intent` call is present on the Builder, it takes precedence over all other sources. + +## How intents relate to the source stream + +The intent operates on the source passed to `sign()`, not on any ingredient added via `add_ingredient`. + +The following diagram shows what happens at sign time for each intent: + +```mermaid +flowchart LR + subgraph CREATE + S1[source stream] --> B1[Builder] + B1 --> O1[signed output] + B1 -. adds .-> A1["c2pa.created action + + digital source type"] + end +``` + +```mermaid +flowchart LR + subgraph EDIT + S2[source stream] --> B2[Builder] + B2 --> O2[signed output] + S2 -. auto-created as .-> P2[parentOf ingredient] + P2 --> B2 + B2 -. adds .-> A2["c2pa.opened action + linked to parent"] + end +``` + +```mermaid +flowchart LR + subgraph UPDATE + S3[source stream] --> B3[Builder] + B3 --> O3[signed output] + S3 -. auto-created as .-> P3[parentOf ingredient] + P3 --> B3 + B3 -. adds .-> A3["c2pa.opened action + linked to parent"] + B3 -. restricts .-> R3[content must not change] + end +``` + +For **EDIT** and **UPDATE**, the Builder looks at the source stream, and if no `parentOf` ingredient has been added manually, it automatically creates one from that stream (and adds the needed action). The source stream *becomes* the parent ingredient. If a `parentOf` ingredient has already been added manually (via `add_ingredient`), the Builder uses that one instead and does not auto-create one from the source. + +### How intent relates to `add_ingredient` + +The intent controls what the Builder does with the source stream at sign time. The `add_ingredient` method adds other ingredients explicitly. These are separate concerns. + +```mermaid +flowchart TD + Intent["Intent + (via Context, set_intent, + or load_settings)"] --> Q{Intent type?} + Q --> |CREATE| CreateFlow["No parent allowed + Source stream is new content"] + Q --> |EDIT or UPDATE| EditFlow{Was a parentOf ingredient + added via add_ingredient?} + EditFlow --> |No| Auto["Builder auto-creates + parentOf from source stream"] + EditFlow --> |Yes| Manual["Builder uses the + manually-added parent"] + Auto --> Opened["Builder adds c2pa.opened + action linked to parent"] + Manual --> Opened + CreateFlow --> Created["Builder adds c2pa.created + action + digital source type"] + + AddIngredient["add_ingredient()"] --> IngType{relationship?} + IngType --> |parentOf| ParentIng["Overrides auto-parent + for EDIT/UPDATE"] + IngType --> |componentOf| CompIng["Additional ingredient + not affected by intent"] + ParentIng --> EditFlow +``` + +## Import + +Intents and digital source types are provided as enums by two imports. + +```py +from c2pa import ( + C2paBuilderIntent, + C2paDigitalSourceType, +) +``` + +## Intent types + +| Intent | Operation | Parent ingredient | Auto-generated action | +| --- | --- | --- | --- | +| `CREATE` | Brand-new content | Must NOT have one | `c2pa.created` | +| `EDIT` | Modifying existing content | Auto-created from the source stream if not provided | `c2pa.opened` (linked to parent) | +| `UPDATE` | Metadata-only changes | Auto-created from the source stream if not provided | `c2pa.opened` (linked to parent) | + +## Choosing the right intent + +```mermaid +flowchart TD + Start([Start]) --> HasParent{Does the asset have + prior history?} + HasParent --> |No| IsNew[Brand-new content] + IsNew --> CREATE["Use CREATE + + C2paDigitalSourceType"] + HasParent --> |Yes| ContentChanged{Will the content + itself change?} + ContentChanged --> |Yes| EDIT[Use EDIT] + ContentChanged --> |No, metadata only| UPDATE[Use UPDATE] + ContentChanged --> |Need full manual control| MANUAL["Skip intents. + Define actions and ingredients + directly in manifest JSON."] +``` + +## CREATE intent + +Use `CREATE` when the asset has no prior history. A `C2paDigitalSourceType` is required to describe how the asset was produced. The Builder will: + +- Add a `c2pa.created` action with the specified digital source type. +- Reject the operation if a `parentOf` ingredient exists. + +### Example: New digital creation + +Using `Context`: + +```py +ctx = Context.from_dict({ + "builder": {"intent": {"Create": "digitalCreation"}} +}) + +with Builder({}, context=ctx) as builder: + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +Using `set_intent`: + +```py +with Builder({}) as builder: + builder.set_intent( + C2paBuilderIntent.CREATE, + C2paDigitalSourceType.DIGITAL_CREATION, + ) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +### Example: Marking AI-generated content + +```py +ctx = Context.from_dict({ + "builder": {"intent": {"Create": "trainedAlgorithmicMedia"}} +}) + +with Builder({}, context=ctx) as builder: + with open("ai_output.jpg", "rb") as source, open("signed_ai_output.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +### Example: CREATE with additional manifest metadata + +A `Context` and a manifest definition can be combined. The context handles the intent; the manifest definition provides additional metadata and assertions: + +```py +ctx = Context.from_dict({ + "builder": { + "intent": {"Create": "digitalCapture"}, + "claim_generator_info": {"name": "an_app", "version": "0.1.0"}, + } +}) + +manifest_def = { + "title": "My New Image", + "assertions": [ + { + "label": "cawg.training-mining", + "data": { + "entries": { + "cawg.ai_inference": {"use": "notAllowed"}, + "cawg.ai_generative_training": {"use": "notAllowed"}, + } + }, + } + ], +} + +with Builder(manifest_def, context=ctx) as builder: + with open("photo.jpg", "rb") as source, open("signed_photo.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +## EDIT intent + +Use `EDIT` when modifying an existing asset. The Builder will: + +1. Check if a `parentOf` ingredient has already been added. If not, it automatically creates one from the source stream passed to `sign()`. +2. Add a `c2pa.opened` action linked to the parent ingredient. + +No `digital_source_type` parameter is needed. + +### Example: Editing an asset + +Using `Context`: + +```py +ctx = Context.from_dict({"builder": {"intent": "edit"}}) + +with Builder({}, context=ctx) as builder: + with open("original.jpg", "rb") as source, open("edited.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +Using `set_intent`: + +```py +with Builder({}) as builder: + builder.set_intent(C2paBuilderIntent.EDIT) + + # The Builder reads "original.jpg" as the parent ingredient, + # then writes the new manifest into "edited.jpg" + with open("original.jpg", "rb") as source, open("edited.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +The resulting manifest contains one ingredient with `relationship: "parentOf"` pointing to `original.jpg` and a `c2pa.opened` action referencing that ingredient. If the source file already has a C2PA manifest, the ingredient preserves the full provenance chain. + +### Example: Editing with a manually-added parent + +To control the parent ingredient's metadata (for example, to set a title or use a different source), add it explicitly: + +```py +ctx = Context.from_dict({"builder": {"intent": "edit"}}) + +with Builder({}, context=ctx) as builder: + with open("original.jpg", "rb") as original: + builder.add_ingredient( + {"title": "Original Photo", "relationship": "parentOf"}, + "image/jpeg", + original, + ) + + with open("canvas.jpg", "rb") as source, open("edited.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +### Example: Editing with additional component ingredients + +A parent ingredient can be combined with component or input ingredients. The intent creates the `c2pa.opened` action for the parent; additional actions can reference components (`componentOf`) or inputs (`inputTo`): + +```py +ctx = Context.from_dict({"builder": {"intent": "edit"}}) + +with Builder({ + "assertions": [ + { + "label": "c2pa.actions.v2", + "data": { + "actions": [ + { + "action": "c2pa.placed", + "parameters": {"ingredientIds": ["overlay_label"]}, + } + ] + }, + } + ], +}, context=ctx) as builder: + + # The Builder auto-creates a parent from the source stream + # and generates a c2pa.opened action for it + + # Add a component ingredient manually + with open("overlay.png", "rb") as overlay: + builder.add_ingredient( + { + "title": "overlay.png", + "relationship": "componentOf", + "label": "overlay_label", + }, + "image/png", + overlay, + ) + + with open("original.jpg", "rb") as source, open("composite.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +## UPDATE intent + +Use `UPDATE` for metadata-only changes where the asset content itself is not modified. This is a restricted form of `EDIT`: + +- Allows exactly one ingredient (only the parent). +- Does not allow changes to the parent's hashed content. +- Produces a more compact manifest than `EDIT`. + +As with `EDIT`, the Builder auto-creates a parent ingredient from the source stream if one is not provided. + +### Example: Adding metadata to a signed asset + +Using `Context`: + +```py +ctx = Context.from_dict({"builder": {"intent": "update"}}) + +with Builder({}, context=ctx) as builder: + with open("signed_asset.jpg", "rb") as source, open("updated_asset.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +Using `set_intent`: + +```py +with Builder({}) as builder: + builder.set_intent(C2paBuilderIntent.UPDATE) + + with open("signed_asset.jpg", "rb") as source, open("updated_asset.jpg", "wb") as dest: + builder.sign(signer, "image/jpeg", source, dest) +``` + +## Intent values in settings + +When configuring settings, the intent is specified as a string or object in the `builder.intent` field: + +| Intent | Settings value | With digital source type | +|--------|---------------|--------------------------| +| Create | `{"Create": ""}` | Required. E.g., `{"Create": "digitalCapture"}` | +| Edit | `"edit"` | Not applicable | +| Update | `"update"` | Not applicable | + +## API reference + +### `Builder.set_intent(intent, digital_source_type=C2paDigitalSourceType.EMPTY)` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `intent` | `C2paBuilderIntent` | The intent: `CREATE`, `EDIT`, or `UPDATE`. | +| `digital_source_type` | `C2paDigitalSourceType` | Required for `CREATE`. Describes how the asset was made. Defaults to `EMPTY`. | + +Raises `C2paError` if the intent cannot be set (for example, a `parentOf` ingredient exists with `CREATE`). diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md new file mode 100644 index 00000000..5ffa140f --- /dev/null +++ b/docs/selective-manifests.md @@ -0,0 +1,805 @@ +# Selective manifest construction + +You can use `Builder` and `Reader` together to selectively construct manifests—keeping only the parts you need and omitting the rest. This is useful when you don't want to include all ingredients in a working store (for example, when some ingredient assets are not visible). + +This process is best described as *filtering* or *rebuilding* a working store: + +1. Read an existing manifest. +2. Choose which elements to retain. +3. Build a new manifest containing only those elements. + +A manifest is a signed data structure attached to an asset that records provenance and which source assets (ingredients) contributed to it. It contains assertions (statements about the asset), ingredients (references to other assets), and references to binary resources (such as thumbnails). + +Since both `Reader` and `Builder` are **read-only** by design (neither has a `remove()` method), to exclude content you must **read what exists, filter to keep what you need, and create a new** `Builder` **with only that information**. This produces a new `Builder` instance—a "rebuild." + +> [!IMPORTANT] +> This process always creates a new `Builder`. The original signed asset and its manifest are never modified, neither is the starting working store. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest based on extracted data. + +## Core concepts + +```mermaid +flowchart LR + A[Signed Asset] -->|Reader| B[JSON + Resources] + B -->|Filter| C[Filtered Data] + C -->|new Builder| D[New Builder] + D -->|sign| E[New Asset] +``` + + + +The fundamental workflow is: + +1. **Read** the existing manifest with `Reader` to get JSON and binary resources +2. **Identify and filter** the parts to keep (parse the JSON, select and gather elements) +3. **Create a new `Builder`** with only the selected parts based on the applied filtering rules +4. **Sign** the new `Builder` into the output asset + +## Reading an existing manifest + +Use `Reader` with a `Context` to extract the manifest store JSON and any binary resources (thumbnails, manifest data). The source asset is never modified. The context is used for trust configuration (which certificates are trusted when validating signatures) and verification settings. See [Configuring Reader](../context.md#configuring-reader) and [Trust configuration](../context.md#trust-configuration) for details. + +```py +ctx = Context.from_dict({ + "trust": { + "user_anchors": "-----BEGIN CERTIFICATE-----\nMIICEzCCA...\n-----END CERTIFICATE-----", + }, + "verify": { + "verify_trust": True + } +}) + +with open("signed_asset.jpg", "rb") as source: + with Reader("image/jpeg", source, context=ctx) as reader: + manifest_store = json.loads(reader.json()) + active_label = manifest_store["active_manifest"] + manifest = manifest_store["manifests"][active_label] +``` + +### Extracting binary resources + +The JSON returned by `reader.json()` only contains string identifiers (JUMBF URIs) for binary data like thumbnails and ingredient manifest stores. Extract the actual binary content by using `resource_to_stream()`: + +```py +# Extract a thumbnail to an in-memory stream +thumb_stream = io.BytesIO() +reader.resource_to_stream(thumbnail_id, thumb_stream) + +# Or extract to a file +with open("thumbnail.jpg", "wb") as f: + reader.resource_to_stream(thumbnail_id, f) +``` + +## Filtering into a new Builder + +> [!NOTE] +> All examples on this page use `Context` with `Reader` and `Builder`. For `Reader`, the context provides trust configuration and verification settings: `Reader(format, source, context=ctx)`. For `Builder`, the context provides custom settings (thumbnails, claim generator, intent): `Builder(manifest_json, context=ctx)`. When a signer is configured in the context, `builder.sign()` is called without a signer instance. See [Context](../context.md) for details. + +Each example below creates a **new `Builder`** from filtered data. The original asset and its manifest store are never modified. + +When transferring ingredients from a `Reader` to a new `Builder`, you must transfer both the JSON metadata and the associated binary resources (thumbnails, manifest data). The JSON contains identifiers that reference those resources; the same identifiers must be used when calling `builder.add_resource()`. + +### Transferring binary resources + +Since ingredients reference binary data (thumbnails, manifest stores), you need to copy those resources from the `Reader` to the new `Builder`. This helper function encapsulates the pattern: + +```py +def transfer_ingredient_resources(reader, builder, ingredients): + """Copy binary resources for a list of ingredients from reader to builder.""" + for ingredient in ingredients: + for key in ("thumbnail", "manifest_data"): + if key in ingredient: + uri = ingredient[key]["identifier"] + buf = io.BytesIO() + reader.resource_to_stream(uri, buf) + buf.seek(0) + builder.add_resource(uri, buf) +``` + +This function is used throughout the examples below. + +### Keep only specific ingredients + +```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + +with open("signed_asset.jpg", "rb") as source: + with Reader("image/jpeg", source, context=ctx) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + + # Filter: keep only ingredients with a specific relationship + kept = [ + ing for ing in active["ingredients"] + if ing["relationship"] == "parentOf" + ] + + # Create a new Builder with only the kept ingredients + with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "ingredients": kept, + }, context=ctx) as new_builder: + transfer_ingredient_resources(reader, new_builder, kept) + + source.seek(0) + with open("output.jpg", "wb") as dest: + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + new_builder.sign("image/jpeg", source, dest) +``` + +### Keep only specific assertions + +```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + +with open("signed_asset.jpg", "rb") as source: + with Reader("image/jpeg", source, context=ctx) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + + # Keep training-mining assertions, filter out everything else + kept = [ + a for a in active["assertions"] + if a["label"] == "cawg.training-mining" + ] + + with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "assertions": kept, + }, context=ctx) as new_builder: + source.seek(0) + with open("output.jpg", "wb") as dest: + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + new_builder.sign("image/jpeg", source, dest) +``` + +### Start fresh and preserve provenance + +Sometimes all existing assertions and ingredients may need to be discarded but the provenance chain should be maintained nevertheless. This is done by creating a new `Builder` with a new manifest definition and adding the original signed asset as an ingredient using `add_ingredient()`. + +The function `add_ingredient()` does not copy the original's assertions into the new manifest. Instead, it stores the original's entire manifest store as opaque binary data inside the ingredient record. This means: + +- The new manifest has its own, independent set of assertions +- The original's full manifest is preserved inside the ingredient, so validators can inspect the full provenance history +- The provenance chain is unbroken: anyone reading the new asset can follow the ingredient link back to the original + +```mermaid +flowchart TD + subgraph Original["Original Signed Asset"] + OA["Assertions: A, B, C"] + OI["Ingredients: X, Y"] + end + subgraph NewBuilder["New Builder"] + NA["Assertions: (empty or new)"] + NI["Ingredient: original.jpg (contains full original manifest as binary data)"] + end + Original -->|"add_ingredient()"| NI + NI -.->|"validators can trace back"| Original + + style NA fill:#efe,stroke:#090 + style NI fill:#efe,stroke:#090 +``` + + + +```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + +with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "assertions": [], +}, context=ctx) as new_builder: + # Add the original as an ingredient to preserve provenance chain. + # add_ingredient() stores the original's manifest as binary data inside + # the ingredient, but does NOT copy the original's assertions. + with open("original_signed.jpg", "rb") as original: + new_builder.add_ingredient( + {"title": "original.jpg", "relationship": "parentOf"}, + "image/jpeg", + original, + ) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a Signer explicitly. + new_builder.sign("image/jpeg", source, dest) +``` + +## Adding actions to a working store + +Actions record what was done to an asset (e.g., color adjustments, cropping, placing content). Use `builder.add_action()` to add them to a working store. + +```py +builder.add_action({ + "action": "c2pa.color_adjustments", + "parameters": {"name": "brightnesscontrast"}, +}) + +builder.add_action({ + "action": "c2pa.filtered", + "parameters": {"name": "A filter"}, + "description": "Filtering applied", +}) +``` + +### Action JSON fields + + +| Field | Required | Description | +| --- | --- | --- | +| `action` | Yes | Action identifier, e.g. `"c2pa.created"`, `"c2pa.opened"`, `"c2pa.placed"`, `"c2pa.color_adjustments"`, `"c2pa.filtered"` | +| `parameters` | No | Free-form object with action-specific data (including `ingredientIds` for linking ingredients, for instance) | +| `description` | No | Human-readable description of what happened | +| `digitalSourceType` | Sometimes, depending on action | URI describing the digital source type (typically for `c2pa.created`) | + + +### Linking actions to ingredients + +When an action involves a specific ingredient, the ingredient is linked to the action using `ingredientIds` (in the action's `parameters`), referencing a matching key in the ingredient. + +#### How `ingredientIds` resolution works + +The SDK matches each value in `ingredientIds` against ingredients using this priority: + +1. `label` on the ingredient (primary): if set and non-empty, this is used as the linking key. +2. `instance_id` on the ingredient (fallback): used when `label` is absent or empty. + +#### Linking with `label` + +The `label` field on an ingredient is the **primary** linking key. Set a `label` on the ingredient and reference it in the action's `ingredientIds`. The label can be any string: it acts as a linking key between the ingredient and the action. + +```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + +manifest_json = { + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "assertions": [ + { + "label": "c2pa.actions.v2", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation", + }, + { + "action": "c2pa.placed", + "parameters": { + "ingredientIds": ["c2pa.ingredient.v3"] + }, + }, + ] + }, + } + ], +} + +with Builder(manifest_json, context=ctx) as builder: + # The label on the ingredient matches the value in ingredientIds + with open("photo.jpg", "rb") as photo: + builder.add_ingredient( + { + "title": "photo.jpg", + "format": "image/jpeg", + "relationship": "componentOf", + "label": "c2pa.ingredient.v3", + }, + "image/jpeg", + photo, + ) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + builder.sign("image/jpeg", source, dest) +``` + +##### Linking multiple ingredients + +When linking multiple ingredients, each ingredient needs a unique label. + +> [!NOTE] +> The labels used for linking in the working store may not be the exact labels that appear in the signed manifest. They are indicators for the SDK to know which ingredient to link with which action. The SDK assigns final labels during signing. + +```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + +manifest_json = { + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "assertions": [ + { + "label": "c2pa.actions.v2", + "data": { + "actions": [ + { + "action": "c2pa.opened", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation", + "parameters": { + "ingredientIds": ["c2pa.ingredient.v3_1"] + }, + }, + { + "action": "c2pa.placed", + "parameters": { + "ingredientIds": ["c2pa.ingredient.v3_2"] + }, + }, + ] + }, + } + ], +} + +with Builder(manifest_json, context=ctx) as builder: + # parentOf ingredient linked to c2pa.opened + with open("original.jpg", "rb") as original: + builder.add_ingredient( + { + "title": "original.jpg", + "format": "image/jpeg", + "relationship": "parentOf", + "label": "c2pa.ingredient.v3_1", + }, + "image/jpeg", + original, + ) + + # componentOf ingredient linked to c2pa.placed + with open("overlay.jpg", "rb") as overlay: + builder.add_ingredient( + { + "title": "overlay.jpg", + "format": "image/jpeg", + "relationship": "componentOf", + "label": "c2pa.ingredient.v3_2", + }, + "image/jpeg", + overlay, + ) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + builder.sign("image/jpeg", source, dest) +``` + +#### Linking with `instance_id` + +When no `label` is set on an ingredient, the SDK matches `ingredientIds` against `instance_id`. + +```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + +# instance_id is used as the linking identifier and must be unique +instance_id = "xmp:iid:939a4c48-0dff-44ec-8f95-61f52b11618f" + +manifest_json = { + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "assertions": [ + { + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.opened", + "parameters": { + "ingredientIds": [instance_id] + }, + } + ] + }, + } + ], +} + +with Builder(manifest_json, context=ctx) as builder: + # No label set: instance_id is used as the linking key + with open("source_photo.jpg", "rb") as photo: + builder.add_ingredient( + { + "title": "source_photo.jpg", + "relationship": "parentOf", + "instance_id": instance_id, + }, + "image/jpeg", + photo, + ) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + builder.sign("image/jpeg", source, dest) +``` + +> [!NOTE] +> The `instance_id` can be read back from the ingredient JSON after signing. + +#### Reading linked ingredients + +After signing, `ingredientIds` is gone. The action's `parameters.ingredients[]` contains hashed JUMBF URIs pointing to ingredient assertions. To match an action to its ingredient, extract the label from the URL: + +```py +ctx = Context.from_dict({"verify": {"verify_trust": True}}) + +with open("signed_asset.jpg", "rb") as signed: + with Reader("image/jpeg", signed, context=ctx) as reader: + manifest_store = json.loads(reader.json()) + active_label = manifest_store["active_manifest"] + manifest = manifest_store["manifests"][active_label] + + # Build a map: label -> ingredient + label_to_ingredient = { + ing["label"]: ing for ing in manifest["ingredients"] + } + + # Match each action to its ingredients by extracting labels from URLs + for assertion in manifest["assertions"]: + if assertion["label"] != "c2pa.actions.v2": + continue + for action in assertion["data"]["actions"]: + for ref in action.get("parameters", {}).get("ingredients", []): + label = ref["url"].rsplit("/", 1)[-1] + matched = label_to_ingredient.get(label) + # matched is the ingredient linked to this action +``` + +#### When to use `label` vs `instance_id` + +| Property | `label` | `instance_id` | +| --- | --- | --- | +| **Who controls it** | Caller (any string) | Caller (any string, or from XMP metadata) | +| **Priority for linking** | Primary: checked first | Fallback: used when label is absent/empty | +| **When to use** | JSON-defined manifests where the caller controls the ingredient definition | Programmatic workflows using XMP-based IDs | +| **Survives signing** | SDK may reassign the actual assertion label | Unchanged | +| **Stable across rebuilds** | The caller controls the build-time value; the post-signing label may change | Yes, always the same set value | + + +**Use `label`** when defining manifests in JSON. +**Use `instance_id`** when working programmatically with ingredients whose identity comes from other sources, or when a stable identifier that persists unchanged across rebuilds is needed. + +## Working with archives + +A `Builder` represents a **working store**: a manifest that is being assembled but has not yet been signed. Archives serialize this working store (definition + resources) to a `.c2pa` binary format, allowing to save, transfer, or resume the work later. For more background on working stores and archives, see [Working stores](https://opensource.contentauthenticity.org/docs/rust-sdk/docs/working-stores). + +There are two distinct types of archives, sharing the same binary format but being conceptually different: builder archives (working store archives) and ingredient archives. + +### Builder archives vs. ingredient archives + +A **builder archive** (also called a working store archive) is a serialized snapshot of a `Builder`. It contains the manifest definition, all resources, and any ingredients that were added. It is created by `builder.to_archive()` and restored with `Builder.from_archive()` to create a new builder instance from an archive, or `builder.with_archive()` to load a working store from a builder archive into an existing builder instance. + +An **ingredient archive** contains the manifest store from an asset that was added as an ingredient. + +The key difference: a builder archive is a work-in-progress (unsigned). An ingredient archive carries the provenance history of a source asset for reuse as an ingredient in other working stores. + +### The ingredients catalog pattern + +An **ingredients catalog** is a collection of archived ingredients that can be selected when constructing a final manifest. Each archive holds ingredients; at build time the caller selects only the ones needed. + +```mermaid +flowchart TD + subgraph Catalog["Ingredients Catalog (archived)"] + A1["Archive: photos.c2pa (ingredients from photo shoot)"] + A2["Archive: graphics.c2pa (ingredients from design assets)"] + A3["Archive: audio.c2pa (ingredients from audio tracks)"] + end + CTX["Context (to propagate settings and configuration)"] + subgraph Build["Final Builder"] + direction TB + SEL["Pick and choose ingredients from any archive in the catalog"] + FB["New Builder with selected ingredients only"] + end + A1 -->|"select photo_1, photo_3"| SEL + A2 -->|"select logo"| SEL + A3 -. "skip (not needed)" .-> X((not used)) + CTX -.->|"settings"| FB + SEL --> FB + FB -->|sign| OUT[Signed Output Asset] + + style A3 fill:#eee,stroke:#999 + style X fill:#f99,stroke:#c00 + style CTX fill:#e8f4fd,stroke:#4a90d9 +``` + + + +```py +ctx = Context.from_dict({ + "builder": { + "thumbnail": {"enabled": False}, + "claim_generator_info": {"name": "an-application", "version": "0.1.0"} + }, + "signer": signer, +}) + +archive_stream.seek(0) +with Reader("application/c2pa", archive_stream, context=ctx) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + + selected = [ + ing for ing in active["ingredients"] + if ing["title"] in {"photo_1.jpg", "logo.png"} + ] + + with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "ingredients": selected, + }, context=ctx) as new_builder: + transfer_ingredient_resources(reader, new_builder, selected) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + new_builder.sign("image/jpeg", source, dest) +``` + +### Overriding ingredient properties + +When adding an ingredient from an archive or from a file, the JSON passed to `add_ingredient()` can override properties like `title` and `relationship`. This is useful when reusing archived ingredients in a different context: + +```py +with open("signed_asset.jpg", "rb") as signed: + builder.add_ingredient( + { + "title": "my-custom-title.jpg", + "relationship": "parentOf", + "instance_id": "my-tracking-id:asset-example-id", + }, + "image/jpeg", + signed, + ) +``` + +The `title`, `relationship`, and `instance_id` fields in the provided JSON take priority. The library fills in the rest (thumbnail, manifest_data, format) from the source. This works with signed assets, `.c2pa` archives, or unsigned files. + +### Using custom vendor parameters in actions + +The C2PA specification allows **vendor-namespaced parameters** on actions using reverse domain notation. These parameters survive signing and can be read back, useful for tagging actions with IDs that support filtering. + +```py +manifest_json = { + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "assertions": [ + { + "label": "c2pa.actions.v2", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeCapture", + "parameters": { + "com.mycompany.tool": "my-editor", + "com.mycompany.session_id": "session-abc-123", + }, + }, + { + "action": "c2pa.placed", + "description": "Placed an image", + "parameters": { + "com.mycompany.layer_id": "layer-42", + "ingredientIds": ["c2pa.ingredient.v3"], + }, + }, + ] + }, + } + ], +} +``` + +After signing, these custom parameters appear alongside the standard fields: + +```json +{ + "action": "c2pa.placed", + "parameters": { + "com.mycompany.layer_id": "layer-42", + "ingredients": [{"url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"}] + } +} +``` + +Custom vendor parameters can be used to filter actions. For example, to find all actions related to a specific layer: + +```py +layer_actions = [ + action for action in actions + if action.get("parameters", {}).get("com.mycompany.layer_id") == "layer-42" +] +``` + +> **Naming convention:** Vendor parameters must use reverse domain notation with period-separated components (e.g., `com.mycompany.tool`, `net.example.session_id`). Some namespaces (e.g., `c2pa` or `cawg`) may be reserved. + +### Extracting ingredients from a working store + +An example workflow is to build up a working store with multiple ingredients, archive it, and then later extract specific ingredients from that archive to use in a new working store. + +```mermaid +flowchart TD + subgraph Step1["Step 1: Build a working store with ingredients"] + IA["add_ingredient(A.jpg)"] --> B1[Builder] + IB["add_ingredient(B.jpg)"] --> B1 + B1 -->|"to_archive()"| AR["archive.c2pa"] + end + subgraph Step2["Step 2: Extract ingredients from archive"] + AR -->|"Reader(application/c2pa)"| RD[JSON + resources] + RD -->|"pick ingredients"| SEL[Selected ingredients] + end + CTX["Context (optional)"] + subgraph Step3["Step 3: Reuse in a new Builder"] + SEL -->|"new Builder + add_resource()"| B2[New Builder] + CTX -.->|"settings"| B2 + B2 -->|sign| OUT[Signed Output] + end + + style CTX fill:#e8f4fd,stroke:#4a90d9 +``` + + + +**Step 1:** Build a working store and archive it: + +```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, +}) + +with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], +}, context=ctx) as builder: + # Add ingredients to the working store + with open("A.jpg", "rb") as ing_a: + builder.add_ingredient( + {"title": "A.jpg", "relationship": "componentOf"}, + "image/jpeg", + ing_a, + ) + + with open("B.jpg", "rb") as ing_b: + builder.add_ingredient( + {"title": "B.jpg", "relationship": "componentOf"}, + "image/jpeg", + ing_b, + ) + + # Save the working store as an archive + archive_stream = io.BytesIO() + builder.to_archive(archive_stream) +``` + +> [!NOTE] +> When restoring from an archive, `with_archive()` preserves context settings while `from_archive()` does not. See [Working with archives](../working-stores.md#working-with-archives) for the full comparison. + +**Step 2:** Read the archive and extract ingredients: + +```py +archive_stream.seek(0) +with Reader("application/c2pa", archive_stream, context=ctx) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + ingredients = active["ingredients"] +``` + +**Step 3:** Create a new Builder with the extracted ingredients: + +```py + sign_ctx = Context.from_dict({ + "builder": { + "thumbnail": {"enabled": False}, + "claim_generator_info": {"name": "an-application", "version": "0.1.0"} + }, + "signer": signer, + }) + + selected = [ing for ing in ingredients if ing["title"] == "A.jpg"] + + with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "ingredients": selected, + }, context=sign_ctx) as new_builder: + transfer_ingredient_resources(reader, new_builder, selected) + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + new_builder.sign("image/jpeg", source, dest) +``` + +### Merging multiple working stores + +> [!NOTE] +> The `Builder` construction and signing in the merge workflow also support `Context`. The caller can pass `context=ctx` to `Builder()` and call `sign()` without a signer argument when the context has one. See [Context](../context.md) for details. + +In some cases it is necessary to merge ingredients from multiple working stores (builder archives) into a single `Builder`. This should be a **fallback strategy**. The recommended practice is to maintain a single active working store and add ingredients incrementally (archived ingredient catalogs help with this). Merging is available when multiple working stores must be consolidated. + +When merging from multiple sources, resource identifier URIs can collide. Rename identifiers with a unique suffix when needed. Use two passes: (1) collect ingredients with collision handling, build the manifest, create the builder; (2) re-read each archive and transfer resources (use original ID for `resource_to_stream()`, renamed ID for `add_resource()` when collisions occurred). + +```py +ctx = Context.from_dict({ + "builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}}, + "signer": signer, +}) + +used_ids: set[str] = set() +suffix_counter = 0 +all_ingredients = [] +archive_ingredient_counts = [] + +# Pass 1: Collect ingredients, renaming IDs on collision +for archive_stream in archives: + archive_stream.seek(0) + with Reader("application/c2pa", archive_stream, context=ctx) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + ingredients = active["ingredients"] + + for ingredient in ingredients: + for key in ("thumbnail", "manifest_data"): + if key not in ingredient: + continue + uri = ingredient[key]["identifier"] + if uri in used_ids: + suffix_counter += 1 + ingredient[key]["identifier"] = f"{uri}__{suffix_counter}" + used_ids.add(ingredient[key]["identifier"]) + all_ingredients.append(ingredient) + + archive_ingredient_counts.append(len(ingredients)) + +with Builder({ + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], + "ingredients": all_ingredients, +}, context=ctx) as builder: + # Pass 2: Transfer resources (match by ingredient index) + offset = 0 + for archive_stream, count in zip(archives, archive_ingredient_counts): + archive_stream.seek(0) + with Reader("application/c2pa", archive_stream, context=ctx) as reader: + manifest_store = json.loads(reader.json()) + active = manifest_store["manifests"][manifest_store["active_manifest"]] + originals = active["ingredients"] + + for original, merged in zip(originals, all_ingredients[offset:offset + count]): + for key in ("thumbnail", "manifest_data"): + if key not in original: + continue + buf = io.BytesIO() + reader.resource_to_stream(original[key]["identifier"], buf) + buf.seek(0) + builder.add_resource(merged[key]["identifier"], buf) + + offset += count + + with open("source.jpg", "rb") as source, open("output.jpg", "wb") as dest: + # In this example, the Signer is on the context. + # A Signer can also be passed as first argument to + # configure a dedicated Signer explicitly. + builder.sign("image/jpeg", source, dest) +``` diff --git a/docs/settings.md b/docs/settings.md new file mode 100644 index 00000000..28bad61b --- /dev/null +++ b/docs/settings.md @@ -0,0 +1,440 @@ +# Using settings + +You can configure SDK settings using a JSON format that controls many aspects of the library's behavior. +The settings JSON format is the same across all languages for the C2PA SDKs (Rust, C/C++, Python, and so on). + +This document describes how to use settings in Python. The Settings schema is the same as the [Rust library](https://github.com/contentauth/c2pa-rs); for the complete JSON schema, see the [Settings reference](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/). + +## Using settings with Context + +The recommended approach is to pass settings to a `Context` object and then use the `Context` with `Reader` and `Builder`. This gives you explicit, isolated configuration with no global state. For details on creating and using contexts, see [Using Context to configure the SDK](context.md). + +**Legacy approach:** The deprecated `load_settings()` function sets global settings. Don't use that approach; instead pass a `Context` (with settings) to `Reader` and `Builder`. See [Using Context with Reader](context.md#configuring-reader) and [Using Context with Builder](context.md#configuring-builder). + +## Settings API + +Create and configure settings: + +| Method | Description | +|--------|-------------| +| `Settings()` | Create default settings with SDK defaults. | +| `Settings.from_json(json_str)` | Create settings from a JSON string. Raises `C2paError` on parse error. | +| `Settings.from_dict(config)` | Create settings from a Python dictionary. | +| `set(path, value)` | Set a single value by dot-separated path (e.g. `"verify.verify_after_sign"`). Value must be a string. Returns `self` for chaining. Use this for programmatic configuration. | +| `update(data)` | Merge JSON configuration into existing settings. `data` can be a JSON string or a dict. Later keys override earlier ones. Use this to apply configuration files or JSON strings. | + +**Important notes:** + +- The `set()` and `update()` methods can be chained for incremental configuration. +- When using multiple configuration methods, later calls override earlier ones (last call wins when same setting is set multiple times). + +```py +from c2pa import Settings + +# Create with defaults +settings = Settings() + +# Set individual values by dot-notation path +settings.set("builder.thumbnail.enabled", "false") + +# Method chaining +settings.set("builder.thumbnail.enabled", "false").set("verify.verify_after_sign", "true") + +# Create from JSON string +settings = Settings.from_json('{"builder": {"thumbnail": {"enabled": false}}}') + +# Create from a dictionary +settings = Settings.from_dict({"builder": {"thumbnail": {"enabled": False}}}) + +# Merge additional configuration +settings.update({"verify": {"remote_manifest_fetch": True}}) +``` + +## Overview of the Settings structure + +The Settings JSON has this top-level structure: + +```json +{ + "version": 1, + "trust": { ... }, + "cawg_trust": { ... }, + "core": { ... }, + "verify": { ... }, + "builder": { ... }, + "signer": { ... }, + "cawg_x509_signer": { ... } +} +``` + +### Settings format + +Settings are provided in **JSON** format only. Pass JSON strings (serialized JSON stings) to `Settings.from_json()` or dictionaries to `Settings.from_dict()`. `from_dict` will convert the dictionary in a format compatible with what the udnerlying native libraries expect. + +```py +# From JSON string +settings = Settings.from_json('{"verify": {"verify_after_sign": true}}') + +# From dict +settings = Settings.from_dict({"verify": {"verify_after_sign": True}}) + +# Context from JSON string +ctx = Context.from_json('{"verify": {"verify_after_sign": true}}') + +# Context from dict +ctx = Context.from_dict({"verify": {"verify_after_sign": True}}) +``` + +To load from a file, read the file contents and pass them to `Settings.from_json()`: + +```py +import json + +with open("config/settings.json", "r") as f: + settings = Settings.from_json(f.read()) +``` + +## Default configuration + +The settings JSON schema — including the complete default configuration with all properties and their default values — is shared by all languages in the SDK: + +```json +{ + "version": 1, + "builder": { + "claim_generator_info": null, + "created_assertion_labels": null, + "certificate_status_fetch": null, + "certificate_status_should_override": null, + "generate_c2pa_archive": true, + "intent": null, + "actions": { + "all_actions_included": null, + "templates": null, + "actions": null, + "auto_created_action": { + "enabled": true, + "source_type": "empty" + }, + "auto_opened_action": { + "enabled": true, + "source_type": null + }, + "auto_placed_action": { + "enabled": true, + "source_type": null + } + }, + "thumbnail": { + "enabled": true, + "ignore_errors": true, + "long_edge": 1024, + "format": null, + "prefer_smallest_format": true, + "quality": "medium" + } + }, + "cawg_trust": { + "verify_trust_list": true, + "user_anchors": null, + "trust_anchors": null, + "trust_config": null, + "allowed_list": null + }, + "cawg_x509_signer": null, + "core": { + "merkle_tree_chunk_size_in_kb": null, + "merkle_tree_max_proofs": 5, + "backing_store_memory_threshold_in_mb": 512, + "decode_identity_assertions": true, + "allowed_network_hosts": null + }, + "signer": null, + "trust": { + "user_anchors": null, + "trust_anchors": null, + "trust_config": null, + "allowed_list": null + }, + "verify": { + "verify_after_reading": true, + "verify_after_sign": true, + "verify_trust": true, + "verify_timestamp_trust": true, + "ocsp_fetch": false, + "remote_manifest_fetch": true, + "skip_ingredient_conflict_resolution": false, + "strict_v1_validation": false + } +} +``` + +## Overview of Settings + +For a complete reference to all the Settings properties, see the [SDK object reference - Settings](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema). + +| Property | Description | +|----------|-------------| +| `version` | Settings format version (integer). The default and only supported value is 1. | +| [`builder`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#buildersettings) | Configuration for Builder. | +| [`cawg_trust`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#trust) | Configuration for CAWG trust lists. | +| [`cawg_x509_signer`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#signersettings) | Configuration for the CAWG x.509 signer. | +| [`core`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#core) | Configuration for core features. | +| [`signer`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#signersettings) | Configuration for the base C2PA signer. | +| [`trust`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#trust) | Configuration for C2PA trust lists. | +| [`verify`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema#verify) | Configuration for verification (validation). | + +The top-level `version` property must be `1`. All other properties are optional. + +For Boolean values, use JSON Booleans `true` and `false` in JSON strings, or Python `True` and `False` when using `from_dict()` or `update()` with a dict. + +> [!IMPORTANT] +> If you don't specify a value for a property, the SDK uses the default value. If you specify a value of `null` (or `None` in a dict), the property is explicitly set to `null`, not the default. This distinction is important when you want to override a default behavior. + +### Trust configuration + +The [`trust` properties](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#trust) control which certificates are trusted when validating C2PA manifests. + +- Using `user_anchors`: recommended for development +- Using `allowed_list` (bypass chain validation) + +| Property | Type | Description | Default | +|----------|------|-------------|---------| +| `trust.allowed_list` | string | Explicitly allowed certificates (PEM format). These certificates are trusted regardless of chain validation. Use for development/testing. | — | +| `trust.trust_anchors` | string | Default trust anchor root certificates (PEM format). **Replaces** the SDK's built-in trust anchors entirely. | — | +| `trust.trust_config` | string | Allowed Extended Key Usage (EKU) OIDs. Controls which certificate purposes are accepted (e.g., document signing: `1.3.6.1.4.1.311.76.59.1.9`). | — | +| `trust.user_anchors` | string | Additional user-provided root certificates (PEM format). Adds custom certificate authorities without replacing the SDK's built-in trust anchors. | — | + +When using self-signed certificates or custom certificate authorities during development, you need to configure trust settings so the SDK can validate your test signatures. + +#### Using `user_anchors` + +For development, you can add your test root CA to the trusted anchors without replacing the SDK's default trust store. +For example: + +```py +with open("test-ca.pem", "r") as f: + test_root_ca = f.read() + +ctx = Context.from_dict({ + "trust": { + "user_anchors": test_root_ca + } +}) + +reader = Reader("signed_asset.jpg", context=ctx) +``` + +#### Using `allowed_list` + +To bypass chain validation, for quick testing, explicitly allow a specific certificate without validating the chain. +For example: + +```py +with open("test_cert.pem", "r") as f: + test_cert = f.read() + +settings = Settings() +settings.update({ + "trust": { + "allowed_list": test_cert + } +}) + +ctx = Context(settings=settings) +reader = Reader("signed_asset.jpg", context=ctx) +``` + +### CAWG trust configuration + +The `cawg_trust` properties configure CAWG (Creator Assertions Working Group) validation of identity assertions in C2PA manifests. The `cawg_trust` object has the same properties as [`trust`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#trust). + +> [!NOTE] +> CAWG trust settings are only used when processing identity assertions with X.509 certificates. If your workflow doesn't use CAWG identity assertions, these settings have no effect. + +### Core + +The [`core` properties](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#core) specify core SDK behavior and performance tuning options. + +Use cases: + +- **Performance tuning for large files**: Set `core.backing_store_memory_threshold_in_mb` to `2048` or higher if processing large video files with sufficient RAM. +- **Restricted network environments**: Set `core.allowed_network_hosts` to limit which domains the SDK can contact. + +### Verify + +The [`verify` properties](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#verify) specify how the SDK validates C2PA manifests. These settings affect both reading existing manifests and verifying newly signed content. + +Common use cases include: + +- [Offline or air-gapped environments](#offline-or-air-gapped-environments). +- [Fast development iteration](#fast-development-iteration) with verification disabled. +- [Strict validation](#strict-validation) for certification or compliance testing. + +By default, the following `verify` properties are `true`, which enables verification: + +- `remote_manifest_fetch` - Fetch remote manifests referenced in the asset. Disable in offline or air-gapped environments. +- `verify_after_reading` - Automatically verify manifests when reading assets. Disable only if you want to manually control verification timing. +- `verify_after_sign` - Automatically verify manifests after signing. Recommended to keep enabled to catch signing errors immediately. +- `verify_timestamp_trust` - Verify timestamp authority (TSA) certificates. WARNING: Disabling this setting makes verification non-compliant. +- `verify_trust` - Verify signing certificates against configured trust anchors. WARNING: Disabling this setting makes verification non-compliant. + +> [!WARNING] +> Disabling verification options (changing `true` to `false`) can make verification non-compliant with the C2PA specification. Only modify these settings in controlled environments or when you have specific requirements. + +#### Offline or air-gapped environments + +Set `remote_manifest_fetch` and `ocsp_fetch` to `false` to disable network-dependent verification features: + +```py +ctx = Context.from_dict({ + "verify": { + "remote_manifest_fetch": False, + "ocsp_fetch": False + } +}) + +reader = Reader("signed_asset.jpg", context=ctx) +``` + +See also [Using Context with Reader](context.md#configuring-reader). + +#### Fast development iteration + +During active development, you can disable verification for faster iteration: + +```py +# WARNING: Only use during development, not in production! +settings = Settings() +settings.set("verify.verify_after_reading", "false") +settings.set("verify.verify_after_sign", "false") + +dev_ctx = Context(settings=settings) +``` + +#### Strict validation + +For certification or compliance testing, enable strict validation: + +```py +ctx = Context.from_dict({ + "verify": { + "strict_v1_validation": True, + "ocsp_fetch": True, + "verify_trust": True, + "verify_timestamp_trust": True + } +}) + +reader = Reader("asset_to_validate.jpg", context=ctx) +validation_result = reader.json() +``` + +### Builder + +The [`builder` properties](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#buildersettings) control how the SDK creates and embeds C2PA manifests in assets. + +#### Claim generator information + +The `claim_generator_info` object identifies your application in the C2PA manifest. **Recommended fields:** + +- `name` (string, required): Your application name (e.g., `"My Photo Editor"`) +- `version` (string, recommended): Application version (e.g., `"2.1.0"`) +- `icon` (string, optional): Icon in C2PA format +- `operating_system` (string, optional): OS identifier or `"auto"` to auto-detect + +**Example:** + +```py +ctx = Context.from_dict({ + "builder": { + "claim_generator_info": { + "name": "My Photo Editor", + "version": "2.1.0", + "operating_system": "auto" + } + } +}) +``` + +#### Thumbnail settings + +The [`builder.thumbnail`](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#thumbnailsettings) properties control automatic thumbnail generation. + +For examples of configuring thumbnails for mobile bandwidth or disabling them for batch processing, see [Controlling thumbnail generation](context.md#controlling-thumbnail-generation). + +#### Action tracking settings + +| Property | Type | Description | Default | +|----------|------|-------------|---------| +| `builder.actions.auto_created_action.enabled` | Boolean | Automatically add a `c2pa.created` action when creating new content. | `true` | +| `builder.actions.auto_created_action.source_type` | string | Source type for the created action. Usually `"empty"` for new content. | `"empty"` | +| `builder.actions.auto_opened_action.enabled` | Boolean | Automatically add a `c2pa.opened` action when opening/reading content. | `true` | +| `builder.actions.auto_placed_action.enabled` | Boolean | Automatically add a `c2pa.placed` action when placing content as an ingredient. | `true` | + +#### Other builder settings + +| Property | Type | Description | Default | +|----------|------|-------------|---------| +| `builder.intent` | object | Claim intent: `{"Create": "digitalCapture"}`, `{"Edit": null}`, or `{"Update": null}`. Describes the purpose of the claim. | `null` | +| `builder.generate_c2pa_archive` | Boolean | Generate content in C2PA archive format. Keep enabled for standard C2PA compliance. | `true` | + +##### Setting Builder intent + +You can use `Context` to set `Builder` intent for different workflows. + +For example, for original digital capture (photos from camera): + +```py +camera_ctx = Context.from_dict({ + "builder": { + "intent": {"Create": "digitalCapture"}, + "claim_generator_info": {"name": "Camera App", "version": "0.1.0"} + } +}) +``` + +Or another example for editing existing content: + +```py +editor_ctx = Context.from_dict({ + "builder": { + "intent": {"Edit": None}, + "claim_generator_info": {"name": "Photo Editor", "version": "0.2.0"} + } +}) +``` + +### Signer + +The [`signer` properties](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#signersettings) configure the primary C2PA signer configuration. Set it to `null` if you provide the signer at runtime, or configure as either a **local** or **remote** signer in settings. + +See [Configuring a signer](context.md#configuring-a-signer) for details on how to configure a Signer. + +#### Local signer + +Use a local signer when you have direct access to the private key and certificate. +For information on all `signer.local` properties, see [signer.local](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#signerlocal) in the SDK object reference. + +#### Remote signer + +Use a remote signer when the private key is stored on a secure signing service (HSM, cloud KMS, and so on). +For information on all `signer.remote` properties, see [signer.remote](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/#signerremote) in the SDK object reference. + +### CAWG X.509 signer configuration + +The `cawg_x509_signer` property specifies configuration for identity assertions. This has the same structure as `signer` (can be local or remote). + +**When to use:** If you need to sign identity assertions separately from the main C2PA claim. When both `signer` and `cawg_x509_signer` are configured, the SDK uses a dual signer: + +- Main claim signature comes from `signer` +- Identity assertions are signed with `cawg_x509_signer` + +For additional JSON configuration examples (minimal configuration, local/remote signer, development/production configurations), see the [Rust SDK settings examples](https://github.com/contentauth/c2pa-rs/blob/main/docs/settings.md#examples). + +## See also + +- [Using Context to configure the SDK](context.md): how to create and use contexts with settings. +- [Usage](usage.md): reading and signing with `Reader` and `Builder`. +- [Rust SDK settings](https://github.com/contentauth/c2pa-rs/blob/main/docs/settings.md): the shared settings schema, default configuration, and JSON examples (language-independent). +- [CAI settings schema reference](https://opensource.contentauthenticity.org/docs/manifest/json-ref/settings-schema/): full schema reference. diff --git a/docs/working-stores.md b/docs/working-stores.md new file mode 100644 index 00000000..1921b809 --- /dev/null +++ b/docs/working-stores.md @@ -0,0 +1,706 @@ +# Manifests, working stores, and archives + +This table summarizes the fundamental entities that you work with when using the CAI SDK. + +| Object | Description | Where it is | Primary API | +|--------|-------------|-------------|-------------| +| [**Manifest store**](#manifest-store) | Final signed provenance data. Contains one or more manifests. | Embedded in asset or remotely in cloud | `Reader` class | +| [**Working store**](#working-store) | Editable in-progress manifest. | `Builder` object | `Builder` class | +| [**Archive**](#archive) | Serialized working store | `.c2pa` file/stream | `Builder.to_archive()` / `Builder.with_archive()` | +| [**Resources**](#working-with-resources) | Binary assets referenced by manifest assertions, such as thumbnails or ingredient thumbnails. | In manifest. | `Builder.add_resource()` / `Reader.resource_to_stream()` | +| [**Ingredients**](#working-with-ingredients) | Source materials used to create an asset. | In manifest. | `Builder.add_ingredient()` | + +This diagram summarizes the relationships among these entities. + +```mermaid +graph TD + subgraph MS["Manifest Store"] + subgraph M1["Manifests"] + R1[Resources] + I1[Ingredients] + end + end + + A[Working Store
Builder object] -->|sign| MS + A -->|to_archive| C[C2PA Archive
.c2pa file] + C -->|with_archive| A +``` + +## Key entities + +### Manifest store + +A _manifest store_ is the data structure that's embedded in (or attached to) a signed asset. It contains one or more manifests that contain provenance data and cryptographic signatures. + +**Characteristics:** + +- Final, immutable signed data embedded in or attached to an asset. +- Contains one or more manifests (identified by URIs). +- Has exactly one `active_manifest` property pointing to the most recent manifest. +- Read it by using a `Reader` object. + +**Example:** When you open a signed JPEG file, the C2PA data embedded in it is the manifest store. + +For more information, see: + +- [Reading manifest stores from assets](#reading-manifest-stores-from-assets) +- [Creating and signing manifests](#creating-and-signing-manifests) +- [Embedded vs external manifests](#embedded-vs-external-manifests) + +### Working store + +A _working store_ is a `Builder` object representing an editable, in-progress manifest that has not yet been signed and bound to an asset. Think of it as a manifest in progress, or a manifest being built. + +**Characteristics:** + +- Editable, mutable state in memory (a Builder object). +- Contains claims, ingredients, and assertions that can be modified. +- Can be saved to a C2PA archive (`.c2pa` JUMBF binary format) for later use. + +**Example:** When you create a `Builder` object and add assertions to it, you're dealing with a working store, as it is an "in progress" manifest being built. + +For more information, see [Using Working stores](#using-working-stores). + +### Archive + +A _C2PA archive_ (or just _archive_) contains the serialized bytes of a working store saved to a file or stream (typically a `.c2pa` file). It uses the standard JUMBF `application/c2pa` format. + +**Characteristics:** + +- Portable serialization of a working store (Builder). +- Save an archive by using `Builder.to_archive()` and restore a full working store from an archive by using `Builder.with_archive()` (with a Builder created from a Context). +- Useful for separating manifest preparation ("work in progress") from final signing. + +For more information, see [Working with archives](#working-with-archives). + +## Reading manifest stores from assets + +Use the `Reader` class to read manifest stores from signed assets. + +### Reading from a file + +```py +from c2pa import Context, Reader + +ctx = Context.from_dict({ + "verify": { + "verify_after_sign": True + } +}) +reader = Reader("signed_image.jpg", context=ctx) +manifest_store_json = reader.json() +``` + +### Reading from a stream + +```py +with open("signed_image.jpg", "rb") as stream: + reader = Reader("image/jpeg", stream, context=ctx) + manifest_json = reader.json() +``` + +For full details on `Context` and `Settings`, see [Using Context to configure the SDK](../context.md). + +### Understanding Reader output + +`Reader.json()` returns a JSON string representing the manifest store. The top-level structure looks like this: + +```json +{ + "active_manifest": "urn:uuid:...", + "manifests": { + "urn:uuid:...": { + "claim_generator": "MyApp/1.0", + "claim_generator_info": [{"name": "MyApp", "version": "0.1.0"}], + "title": "signed_image.jpg", + "assertions": [ + {"label": "c2pa.actions", "data": {"actions": [...]}}, + {"label": "c2pa.hash.data", "data": {...}} + ], + "ingredients": [...], + "signature_info": { + "alg": "Es256", + "issuer": "...", + "time": "2025-01-15T12:00:00Z" + } + } + } +} +``` + +- `active_manifest`: The URI label of the most recent manifest. This is typically the one to inspect first. +- `manifests`: A dictionary of all manifests in the store, keyed by their URI label. Assets that have been re-signed or that contain ingredient history may have multiple manifests. +- Within each manifest: `assertions` contain the provenance statements, `ingredients` list source materials, and `signature_info` provides details about who signed and when. + +The SDK also provides convenience methods to avoid manual JSON parsing: + +```py +reader = Reader("signed_image.jpg", context=ctx) + +# Get the active manifest directly as a dict +active = reader.get_active_manifest() + +# Get a specific manifest by label +manifest = reader.get_manifest("urn:uuid:...") + +# Check validation status +state = reader.get_validation_state() +results = reader.get_validation_results() +``` + +`Reader.detailed_json()` returns a more comprehensive JSON representation with a different structure than `json()`. It is useful when additional details about the manifest internals are needed. + +## Using working stores + +A **working store** is represented by a `Builder` object. It contains "live" manifest data as you add information to it. + +### Creating a working store + +```py +import json +from c2pa import Builder, Context + +manifest_json = json.dumps({ + "claim_generator_info": [{ + "name": "example-app", + "version": "0.1.0" + }], + "title": "Example asset", + "assertions": [] +}) + +ctx = Context.from_dict({ + "builder": { + "thumbnail": {"enabled": True} + } +}) +builder = Builder(manifest_json, context=ctx) +``` + +### Modifying a working store + +Before signing, you can modify the working store (Builder): + +```py +import io + +# Add binary resources (like thumbnails) +with open("thumbnail.jpg", "rb") as thumb: + builder.add_resource("thumbnail", thumb) + +# Add ingredients (source files) +ingredient_json = json.dumps({ + "title": "Original asset", + "relationship": "parentOf" +}) +with open("source.jpg", "rb") as ingredient: + builder.add_ingredient(ingredient_json, "image/jpeg", ingredient) + +# Add actions +action_json = { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" +} +builder.add_action(action_json) + +# Configure embedding behavior +builder.set_no_embed() # Don't embed manifest in asset +builder.set_remote_url("https://example.com/manifests/") +``` + +### From working store to manifest store + +When you sign an asset, the working store (Builder) becomes a manifest store embedded in the output: + +```py +from c2pa import Signer, C2paSignerInfo, C2paSigningAlg, Context + +# Create a signer +signer_info = C2paSignerInfo( + alg=C2paSigningAlg.ES256, + sign_cert=certs, + private_key=private_key, + ta_url=b"http://timestamp.digicert.com" +) +signer = Signer.from_info(signer_info) + +ctx = Context.from_dict({ + "builder": {"thumbnail": {"enabled": True}}, + "signer": signer, +}) +builder = Builder(manifest_json, context=ctx) + +# Sign the asset - working store becomes a manifest store +with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign("image/jpeg", src, dst) + +# Now "signed.jpg" contains a manifest store +# You can read it back with Reader +reader = Reader("signed.jpg", context=ctx) +manifest_store_json = reader.json() +``` + +## Creating and signing manifests + +### Creating a Builder (working store) + +```py +ctx = Context.from_dict({ + "builder": { + "thumbnail": {"enabled": True} + } +}) +builder = Builder(manifest_json, context=ctx) +``` + +### Creating a Signer + +For testing, create a `Signer` with certificates and private key: + +```py +from c2pa import Signer, C2paSignerInfo, C2paSigningAlg + +# Load credentials +with open("certs.pem", "rb") as f: + certs = f.read() +with open("private_key.pem", "rb") as f: + private_key = f.read() + +# Create signer +signer_info = C2paSignerInfo( + alg=C2paSigningAlg.ES256, # ES256, ES384, ES512, PS256, PS384, PS512, ED25519 + sign_cert=certs, # Certificate chain in PEM format + private_key=private_key, # Private key in PEM format + ta_url=b"http://timestamp.digicert.com" # Optional timestamp authority URL +) +signer = Signer.from_info(signer_info) +``` + +**WARNING**: Never hard-code or directly access private keys in production. Use a Hardware Security Module (HSM) or Key Management Service (KMS). + +### Signing an asset + +The Builder must be created with a Context that includes a signer. Then call `sign()` without passing a signer argument: + +```py +try: + with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + manifest_bytes = builder.sign("image/jpeg", src, dst) + print("Signed successfully!") +except Exception as e: + print(f"Signing failed: {e}") +``` + +### Signing with file paths + +You can also sign using file paths directly: + +```py +manifest_bytes = builder.sign_file("source.jpg", "signed.jpg") +``` + +### Complete example + +This code combines the above examples to create, sign, and read a manifest. + +```py +import json +from c2pa import Builder, Reader, Context, Signer, C2paSignerInfo, C2paSigningAlg + +try: + # 1. Define manifest + manifest_json = json.dumps({ + "claim_generator_info": [{"name": "demo-app", "version": "0.1.0"}], + "title": "Signed image", + "assertions": [] + }) + + # 2. Load credentials and create signer + with open("certs.pem", "rb") as f: + certs = f.read() + with open("private_key.pem", "rb") as f: + private_key = f.read() + + signer_info = C2paSignerInfo( + alg=C2paSigningAlg.ES256, + sign_cert=certs, + private_key=private_key, + ta_url=b"http://timestamp.digicert.com" + ) + signer = Signer.from_info(signer_info) + + # 3. Create context with settings and signer + ctx = Context.from_dict({ + "builder": {"thumbnail": {"enabled": True}} + }, signer=signer) + + # 4. Create Builder with context and sign + builder = Builder(manifest_json, context=ctx) + with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign("image/jpeg", src, dst) + + print("Asset signed with context settings") + + # 5. Read back the manifest store + reader = Reader("signed.jpg", context=ctx) + print(reader.json()) + +except Exception as e: + print(f"Error: {e}") +``` + +## Working with resources + +_Resources_ are binary assets referenced by manifest assertions, such as thumbnails or ingredient thumbnails. + +C2PA manifest data is not just JSON. A manifest store also contains binary resources (thumbnails, ingredient data, and other embedded files) that are referenced from the JSON metadata by JUMBF URIs. When `reader.json()` is called, the JSON includes URI references (like `"self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"`) that point to these binary resources. To retrieve the actual binary data, use `reader.resource_to_stream()` with the URI from the JSON. This separation keeps the JSON lightweight while allowing manifests to carry rich binary content alongside the metadata. + +### Understanding resource identifiers + +When you add a resource to a working store (Builder), you assign it an identifier string. When the manifest store is created during signing, the SDK automatically converts this to a proper JUMBF URI. + +**Resource identifier workflow:** + +```mermaid +graph LR + A[Simple identifier
'thumbnail'] -->|add_resource| B[Working Store
Builder] + B -->|sign| C[JUMBF URI
'self#jumbf=...'] + C --> D[Manifest Store
in asset] +``` + +1. **During manifest creation**: You use a string identifier (e.g., `"thumbnail"`, `"thumbnail1"`). +2. **During signing**: The SDK converts these to JUMBF URIs (e.g., `"self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"`). +3. **After signing**: The manifest store contains the full JUMBF URI that you use to extract the resource. + +### Extracting resources from a manifest store + +To extract a resource, you need its JUMBF URI from the manifest store: + +```py +import json + +reader = Reader("signed_image.jpg", context=ctx) +manifest_store = json.loads(reader.json()) + +# Get active manifest +active_uri = manifest_store["active_manifest"] +manifest = manifest_store["manifests"][active_uri] + +# Extract thumbnail if it exists +if "thumbnail" in manifest: + # The identifier is the JUMBF URI + thumbnail_uri = manifest["thumbnail"]["identifier"] + # Example: "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg" + + # Extract to a stream + with open("thumbnail.jpg", "wb") as f: + reader.resource_to_stream(thumbnail_uri, f) + print("Thumbnail extracted") +``` + +### Adding resources to a working store + +When building a manifest, you add resources using identifiers. The SDK will reference these in your manifest JSON and convert them to JUMBF URIs during signing. + +```py +ctx = Context.from_dict({"builder": {"thumbnail": {"enabled": True}}, "signer": signer}) +builder = Builder(manifest_json, context=ctx) + +# Add resource from a stream +with open("thumbnail.jpg", "rb") as thumb: + builder.add_resource("thumbnail", thumb) + +# Sign: the "thumbnail" identifier becomes a JUMBF URI in the manifest store +with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign("image/jpeg", src, dst) +``` + +## Working with ingredients + +### Why ingredients matter + +Ingredients are how C2PA tracks the history of content through edits, compositions, and transformations to build a content provenance chain represented by the manifest store. Adding an ingredient to a manifest creates a verifiable link from the current asset back to its source material. This builds a **provenance chain**: original photo, then edited version, then composite, then published asset, etc. + +The `relationship` field describes how the source (ingredient) was used: `"parentOf"` for a direct edit, `"componentOf"` for an element composited into a larger work, or `"inputTo"` for a general input. This lets verifiers understand not just what the sources were, but how they contributed to the final asset. + +### Overview + +Ingredients represent source materials used to create an asset, preserving the provenance chain. Ingredients themselves can be turned into ingredient archives (`.c2pa`). An ingredient archive is a serialized `Builder` with _exactly one and only one_ ingredient. Once archived with only one ingredient, the Builder archive is an ingredient archive. Such ingredient archives can be used as ingredient in other working stores, as an ingredient archive can be added back directly to a working store (no un-archiving of the ingredient needed, use `application/c2pa` format when adding an ingredient archive to a Builder instance). + +### Adding ingredients to a working store + +When creating a manifest, add ingredients to preserve the provenance chain: + +```py +ctx = Context.from_dict({"builder": {"claim_generator_info": {"name": "my-app", "version": "0.1.0"}}, "signer": signer}) +builder = Builder(manifest_json, context=ctx) + +# Define ingredient metadata +ingredient_json = json.dumps({ + "title": "Original asset", + "relationship": "parentOf" +}) + +# Add ingredient from a stream +with open("source.jpg", "rb") as ingredient: + builder.add_ingredient(ingredient_json, "image/jpeg", ingredient) + +# Sign: ingredients become part of the manifest store +with open("new_asset.jpg", "rb") as src, open("signed_asset.jpg", "w+b") as dst: + builder.sign("image/jpeg", src, dst) +``` + +### Ingredient relationships + +Specify the relationship between the ingredient and the current asset: + +| Relationship | Meaning | +|--------------|---------| +| `parentOf` | The ingredient is a direct parent of this asset | +| `componentOf` | The ingredient is a component used in this asset | +| `inputTo` | The ingredient was an input to creating this asset | + +Example with explicit relationship (builder is created with a Context as in the examples above): + +```py +ingredient_json = json.dumps({ + "title": "Base layer", + "relationship": "componentOf" +}) + +with open("base_layer.png", "rb") as ingredient: + builder.add_ingredient(ingredient_json, "image/png", ingredient) +``` + +## Working with archives + +An _archive_ (C2PA archive) is a serialized working store (`Builder` object) saved to a stream. + +Using archives provides these advantages: + +- **Save work-in-progress**: Persist a working store between sessions. +- **Separate creation from signing**: Prepare manifests on one machine, sign on another. +- **Share manifests**: Transfer working stores between systems. +- **Offline preparation**: Build manifests offline, sign them later. + +The default binary format of an archive is the **C2PA JUMBF binary format** (`application/c2pa`), which is the standard way to save and restore working stores. + +### Saving a working store to archive + +```py +import io + +ctx = Context.from_dict({"builder": {"claim_generator_info": {"name": "my-app", "version": "0.1.0"}}}) +builder = Builder(manifest_json, context=ctx) +with open("thumbnail.jpg", "rb") as thumb: + builder.add_resource("thumbnail", thumb) +with open("source.jpg", "rb") as ingredient: + builder.add_ingredient(ingredient_json, "image/jpeg", ingredient) + +# Save working store to archive stream +archive = io.BytesIO() +builder.to_archive(archive) + +# Or save to a file +with open("manifest.c2pa", "wb") as f: + archive.seek(0) + f.write(archive.read()) + +print("Working store saved to archive") +``` + +A Builder containing **only one ingredient and only the ingredient data** (no other ingredient, no other actions) is an ingredient archive. Ingredient archives can be added directly as ingredient to other working stores too. + +### Restoring a working store from archive + +There are two ways to load a working store from an archive. They differ in whether the builder's current context (settings) is preserved or not. + +#### `with_archive()` + +Use `with_archive()` when you need the restored builder to use specific settings that you put on the Builder on instanciation by using a context as parameter of the Builder constructor. Create a `Builder` with a `Context` first, then call `with_archive()` to load the archived manifest definition into it. The archive replaces only the manifest definition; the builder's context and settings are preserved. + +```py +# Create context with custom settings and signer +ctx = Context.from_dict({ + "builder": { + "thumbnail": {"enabled": False}, + "claim_generator_info": {"name": "My App", "version": "0.1.0"} + }, + "signer": signer, +}) + +# Create builder with context, then load archive into it +with open("manifest.c2pa", "rb") as archive: + builder = Builder({}, context=ctx) + builder.with_archive(archive) + +# The builder has the archived manifest definition +# but keeps the context settings (no thumbnails, custom claim generator) +with open("asset.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign("image/jpeg", src, dst) +``` + +> [!IMPORTANT] +> `with_archive()` replaces the builder's manifest definition with the one from the archive. Any definition passed to `Builder()` on instanciation is discarded. An empty dict `{}` is idiomatic for the initial definition when you plan to load an archive immediately after. + +#### Choosing how to restore from an archive + +Use `with_archive()` so that the restored builder uses your `Context` (custom settings and signer). The archive carries only the manifest definition; it does not store context or settings. By creating a `Builder` with a `Context` and then calling `with_archive()`, you ensure the restored builder keeps your settings. + +| | `with_archive()` | +|---|---| +| **Context preserved** | Yes — settings come from the builder's context | +| **Usage pattern** | `Builder({}, context=ctx).with_archive(stream)` | +| **What the archive carries** | Only the manifest definition (not settings, signer, or context) | + +### Two-phase workflow example + +#### Phase 1: Prepare manifest + +This step prepares the manifest on a Builder, and archives it into a Builder archive for later reuse. + +```py +import io +import json + +ctx = Context.from_dict({"builder": {"claim_generator_info": {"name": "my-app", "version": "0.1.0"}}}) +manifest_json = json.dumps({ + "title": "Artwork draft", + "assertions": [] +}) +builder = Builder(manifest_json, context=ctx) + +with open("thumb.jpg", "rb") as thumb: + builder.add_resource("thumbnail", thumb) +with open("sketch.png", "rb") as sketch: + builder.add_ingredient( + json.dumps({"title": "Sketch"}), "image/png", sketch + ) + +# Save working store as archive +with open("artwork_manifest.c2pa", "wb") as f: + builder.to_archive(f) + +print("Working store saved to artwork_manifest.c2pa") +``` + +#### Phase 2: Sign the asset + +Restore the working store with a Context so that settings (e.g. thumbnails on/off) and the signer are applied: + +```py +ctx = Context.from_dict({ + "builder": {"thumbnail": {"enabled": False}}, + "signer": signer, +}) + +with open("artwork_manifest.c2pa", "rb") as archive: + builder = Builder({}, context=ctx) + builder.with_archive(archive) + +# Sign using the context's signer +with open("artwork.jpg", "rb") as src, open("signed_artwork.jpg", "w+b") as dst: + builder.sign("image/jpeg", src, dst) +``` + +## Embedded vs external manifests + +By default, manifest stores are **embedded** directly into the asset file. You can also use **external** or **remote** manifest stores. + +### Default: embedded manifest stores + +```py +ctx = Context.from_dict({"builder": {"thumbnail": {"enabled": True}}, "signer": signer}) +builder = Builder(manifest_json, context=ctx) + +# Default behavior: manifest store is embedded in the output +with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign("image/jpeg", src, dst) + +# Read it back — manifest store is embedded +reader = Reader("signed.jpg", context=ctx) +``` + +### External manifest stores (no embed) + +Prevent embedding the manifest store in the asset: + +```py +ctx = Context.from_dict({"signer": signer}) +builder = Builder(manifest_json, context=ctx) + +builder.set_no_embed() # Don't embed the manifest store + +# Sign: manifest store is NOT embedded, manifest bytes are returned +with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: + manifest_bytes = builder.sign("image/jpeg", src, dst) + +# manifest_bytes contains the manifest store +# Save it separately (as a sidecar file or upload to server) +with open("output.c2pa", "wb") as f: + f.write(manifest_bytes) + +print("Manifest store saved externally to output.c2pa") +``` + +### Remote manifest stores + +Reference a manifest store stored at a remote URL: + +```py +ctx = Context.from_dict({"signer": signer}) +builder = Builder(manifest_json, context=ctx) + +builder.set_remote_url("https://example.com/manifests/") + +# The asset will contain a reference to the remote manifest store +with open("source.jpg", "rb") as src, open("output.jpg", "w+b") as dst: + builder.sign("image/jpeg", src, dst) +``` + +## Best practices + +### Use Context for configuration + +Use `Context` objects for SDK configuration: + +```py +ctx = Context.from_dict({ + "verify": { + "verify_after_sign": True + }, + "trust": { + "user_anchors": trust_anchors_pem + } +}) + +builder = Builder(manifest_json, context=ctx) +reader = Reader("asset.jpg", context=ctx) +``` + +### Use ingredients to build provenance chains + +Add ingredients to your manifests to maintain a provenance chain: + +```py +ctx = Context.from_dict({"builder": {"claim_generator_info": {"name": "my-app", "version": "0.1.0"}}, "signer": signer}) +builder = Builder(manifest_json, context=ctx) + +ingredient_json = json.dumps({ + "title": "Original source", + "relationship": "parentOf" +}) + +with open("original.jpg", "rb") as ingredient: + builder.add_ingredient(ingredient_json, "image/jpeg", ingredient) + +with open("edited.jpg", "rb") as src, open("signed.jpg", "w+b") as dst: + builder.sign("image/jpeg", src, dst) +``` + +## Additional resources + +- [Manifest reference](https://opensource.contentauthenticity.org/docs/manifest/manifest-ref) +- [X.509 certificates](https://opensource.contentauthenticity.org/docs/c2patool/x_509) +- [Trust lists](https://opensource.contentauthenticity.org/docs/conformance/trust-lists/) +- [CAWG identity](https://cawg.io/identity/)