Skip to main content

Configuring SDK settings

This guide shows you how to configure the C2PA Rust library using the Context API with declarative settings in JSON format.

Overview

The Context structure encapsulates configuration for:

  • Settings: Configuration options for verification, signing, network policies, builder behavior, etc.
  • HTTP resolvers: Customizable sync and async HTTP clients for fetching remote manifests
  • Signers: Cryptographic signers used to sign manifests (created automatically from settings)

Context is thread-safe and can be shared with Arc<Context>, allowing multiple configurations in one application. It replaces the older thread-local Settings pattern with explicit dependencies and better testability.

Quick start

Creating a Context

The simplest way to create a Context is with default settings:

use c2pa::Context;

let context = Context::new();

Loading settings from a file

Load settings from a file using the with_settings() method, which automatically detects the format (JSON or TOML):

use c2pa::{Context, Builder, Result};

fn main() -> Result<()> {
// From a file
let context = Context::new()
.with_settings(include_str!("settings.json"))?;

// Create builder using context settings
let builder = Builder::from_context(context);
Ok(())
}

Loading settings inline

You can also provide settings inline in either JSON or TOML format. For example, using JSON:

use c2pa::{Context, Result};

fn main() -> Result<()> {
// Inline JSON format
let context = Context::new()
.with_settings(r#"
{"verify":
{"verify_after_sign": true}}
"#)?;

Ok(())
}

Loading settings from a struct

use c2pa::{Context, Settings, Result};

fn main() -> Result<()> {
let mut settings = Settings::default();
settings.verify.verify_after_sign = true;

let context = Context::new().with_settings(settings)?;
Ok(())
}

Using Context

With Reader

Reader uses Context to control manifest validation and remote resource fetching:

use c2pa::{Context, Reader, Result};
use std::fs::File;

fn main() -> Result<()> {
// Configure context
let context = Context::new()
.with_settings(r#"{"verify": {"remote_manifest_fetch": false}}"#)?;

// Create reader with context
let stream = File::open("path/to/image.jpg")?;
let reader = Reader::from_context(context)
.with_stream("image/jpeg", stream)?;

println!("{}", reader.json());
Ok(())
}

With Builder

Builder uses Context to configure signing operations. The Context automatically creates a signer from settings when needed:

use c2pa::{Context, Builder, Result};
use std::io::Cursor;
use serde_json::json;

fn main() -> Result<()> {
// Configure context with signer and builder settings
let context = Context::new()
.with_settings(json!({
"signer": {
"local": {
"alg": "ps256",
"sign_cert": "path/to/cert.pem",
"private_key": "path/to/key.pem",
"tsa_url": "http://timestamp.digicert.com"
}
},
"builder": {
"claim_generator_info": {"name": "My App"},
"intent": {"Create": "digitalCapture"}
}
}))?;

// Create builder with context and inline JSON definition
let mut builder = Builder::from_context(context)
.with_definition(json!({"title": "My Image"}))?;

// Save with automatic signer from context
let mut source = std::fs::File::open("source.jpg")?;
let mut dest = Cursor::new(Vec::new());
builder.save_to_stream("image/jpeg", &mut source, &mut dest)?;

Ok(())
}

When the manifest JSON omits claim_generator_info (or uses []), Builder resolves it at claim time: first from builder.claim_generator_info in the settings you passed to Context (as in the json! above, if you set that field), otherwise the library default.

Settings definition

The Settings definition has the following top-level structure:

{
"version": 1,
"trust": { ... },
"cawg_trust": { ... },
"core": { ... },
"verify": { ... },
"builder": { ... },
"signer": { ... },
"cawg_x509_signer": { ... }
}
note
  • All properties are optional. If you do not specify a value, the SDK will use the default value, if any.
  • If you specify a value of null, then the property will be set to null, not the default.
  • Do not quote Boolean property values (for example, use true not "true").

For a complete reference to all the Settings properties, see the SDK object reference: Settings.

PropertyDescription
versionSettings format version (integer). The default and only supported value is 1.
builderConfiguration for Builder.
cawg_trustConfiguration for CAWG trust lists.
cawg_x509_signerConfiguration for the CAWG x.509 signer.
coreConfiguration for core features.
signerConfiguration for the base C2PA signer.
trustConfiguration for C2PA trust lists.
verifyConfiguration for verification (validation).

Default configuration

Here's the Settings JSON with all default values:

{
"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,
"trusted_ica_issuers": 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,
"max_decompressed_manifest_size_in_mb": 32
},
"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
}
}

Configuration examples

Minimal configuration

{
"version": 1,
"builder": {
"claim_generator": {
"name": "my app",
"version": "0.1"
},
"intent": {"Create": "digitalCapture"}
}
}

Local signer

{
"version": 1,
"signer": {
"local": {
"alg": "ps256",
"sign_cert": "-----BEGIN CERTIFICATE-----\nMIIExample...\n-----END CERTIFICATE-----",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIExample...\n-----END PRIVATE KEY-----",
"tsa_url": "http://timestamp.digicert.com"
}
},
"builder": {
"intent": {"Create": "digitalCapture"}
}
}

Remote signer

{
"version": 1,
"signer": {
"remote": {
"url": "https://my-signing-service.com/sign",
"alg": "ps256",
"sign_cert": "-----BEGIN CERTIFICATE-----\nMIIExample...\n-----END CERTIFICATE-----",
"tsa_url": "http://timestamp.digicert.com"
}
}
}

CAWG dual signer

{
"version": 1,
"signer": {
"local": {
"alg": "ps256",
"sign_cert": "-----BEGIN CERTIFICATE-----\nC2PA Cert...\n-----END CERTIFICATE-----",
"private_key": "-----BEGIN PRIVATE KEY-----\nC2PA Key...\n-----END PRIVATE KEY-----"
}
},
"cawg_x509_signer": {
"local": {
"alg": "es256",
"sign_cert": "-----BEGIN CERTIFICATE-----\nCAWG Cert...\n-----END CERTIFICATE-----",
"private_key": "-----BEGIN PRIVATE KEY-----\nCAWG Key...\n-----END PRIVATE KEY-----"
}
}
}

Development configuration

{
"version": 1,
"verify": {
"verify_trust": false,
"verify_timestamp_trust": false
},
"builder": {
"thumbnail": {
"enabled": false
}
}
}

Production configuration

{
"version": 1,
"trust": {
"trust_anchors": "-----BEGIN CERTIFICATE-----\n...",
"trust_config": "1.3.6.1.5.5.7.3.4\n1.3.6.1.5.5.7.3.36"
},
"core": {
"backing_store_memory_threshold_in_mb": 1024
},
"builder": {
"intent": {"Create": "digitalCapture"},
"thumbnail": {
"long_edge": 512,
"quality": "high"
}
}
}

Configuring signers

In most cases, you don't need to explicitly set a signer on the Context. Instead, configure signer settings in your configuration, and Context will create the signer automatically when you call save_to_stream() or save_to_file().

From settings

This is the recommended approach. Configure signer settings in the settings file, for example in JSON:

{
"signer": {
"local": {
"alg": "ps256",
"sign_cert": "path/to/cert.pem",
"private_key": "path/to/key.pem",
"tsa_url": "http://timestamp.example.com"
}
}
}

Then use it with Builder:

use c2pa::{Context, Builder, Result};
use serde_json::json;

fn main() -> Result<()> {
// Configure context with signer settings
let context = Context::new()
.with_settings(include_str!("config.json"))?;

let mut builder = Builder::from_context(context)
.with_definition(json!({"title": "My Image"}))?;

// Signer is created automatically from context's settings
let mut source = std::fs::File::open("source.jpg")?;
let mut dest = std::fs::File::create("signed.jpg")?;
builder.save_to_stream("image/jpeg", &mut source, &mut dest)?;

Ok(())
}

Custom signer

For advanced use cases like HSMs or custom signing logic, you can create and set a custom signer:

use c2pa::{Context, create_signer, SigningAlg, Result};

fn main() -> Result<()> {
// Explicitly create a signer
let signer = create_signer::from_files(
"path/to/cert.pem",
"path/to/key.pem",
SigningAlg::Ps256,
None
)?;

// Set it on the context
let context = Context::new().with_signer(signer);

// Later retrieve it
let signer_ref = context.signer()?;

Ok(())
}

Signer configuration options

The signer field in Settings supports two types: local and remote.

Local signer: for local certificate and private key.

note

Using a local signer is suitable primarily for development and testing, not production because values for sign_cert and private_key must be inline PEM strings.

...
"signer": {
"local": {
"alg": "ps256", // Signing algorithm
"sign_cert": "cert.pem", // PEM string
"private_key": "key.pem", // PEM string
"tsa_url": "http://timestamp.digicert.com" // Optional timestamp authority URL
}
},
...

Remote signer: for remote signing services.

"signer": {
"remote": {
"alg": "ps256", // Signing algorithm
"url": "https://my-signing-service.com/sign", // Signing service URL
"sign_cert": "cert.pem", // Path to cert file for verification
"tsa_url": "http://timestamp.digicert.com" // Optional timestamp authority URL
}
}

Advanced topics

Custom HTTP resolvers

For advanced use cases, you can provide custom HTTP resolvers to control how remote manifests are fetched. Custom resolvers are useful for adding authentication, caching, logging, or mocking network calls in tests.

Thread safety

Context is designed to be used safely across threads. While Context itself doesn't implement Clone, you can:

  1. Create separate contexts for different threads
  2. Use Arc<Context> to share a context across threads (for read-only access)
  3. Pass contexts by reference where appropriate

When to use Context sharing

Understanding when to use a shared Context helps optimize your application:

Use single-use Context (no Arc needed):

  • Single signing operation
  • Single reading operation
  • Each operation has different configuration needs
use c2pa::{Context, Builder, Reader};

// For simple, single-use cases create a fresh Context per operation
let builder = Builder::from_context(Context::new().with_settings(config)?);
// or, for reading:
let reader = Reader::from_context(Context::new().with_settings(config)?);

Use shared Context (with Arc):

  • Multi-threaded operations
  • Multiple builders or readers using the same configuration
  • Signing and reading with the same settings
  • Web servers handling multiple requests with shared configuration
use std::sync::Arc;

// Shared configuration
let ctx = Arc::new(Context::new().with_settings(config)?);
let builder1 = Builder::from_shared_context(&ctx);
let builder2 = Builder::from_shared_context(&ctx);

Migration from thread-local Settings

The Context API replaces the older thread-local Settings pattern. If you're migrating existing code, this section explains how the two approaches differ.

Backwards compatibility

Settings still works: The Settings type and its configuration format remain unchanged. All your existing settings files (JSON or TOML) work with Context without modification.

Key differences:

AspectOld Thread-Local SettingsNew Context API
ScopeGlobal, affects all operationsPer-operation, explicitly passed
Thread SafetyNot thread-safeThread-safe, shareable with Arc
ConfigurationSet once per threadCan have multiple configurations
TestabilityDifficult (thread-local state)Easy (isolated contexts)

Migration examples

Old approach (deprecated):

use c2pa::Settings;

// Global settings affect all operations
Settings::from_toml(include_str!("settings.toml"))?;
let reader = Reader::from_stream("image/jpeg", stream)?;

New approach with Context:

use c2pa::{Context, Reader};

// Explicit context per operation
let context = Context::new()
.with_settings(include_str!("settings.toml"))?;
let reader = Reader::from_context(context)
.with_stream("image/jpeg", stream)?;

Multiple configurations (impossible with thread-local settings):

use c2pa::{Context, Builder};

// Development signer for testing
let dev_ctx = Context::new()
.with_settings(include_str!("dev_settings.toml"))?;
let dev_builder = Builder::from_context(dev_ctx);

// Production signer for real signing
let prod_ctx = Context::new()
.with_settings(include_str!("prod_settings.toml"))?;
let prod_builder = Builder::from_context(prod_ctx);

How Context uses Settings internally

Context wraps a Settings instance and uses it to:

  1. Create signers automatically: When you call context.signer() or builder.save_to_stream(), the Context creates a signer from the signer field in Settings (if present).

  2. Configure HTTP resolvers: The Context creates default HTTP resolvers (for fetching remote manifests) and applies the core.allowed_network_hosts setting from Settings.

  3. Control verification: The verify settings control how manifests are validated.

  4. Configure builder behavior: The builder settings control thumbnail generation, actions, and other manifest creation options.

The Settings format hasn't changed; only how you provide those settings has changed.