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": { ... }
}
- 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 tonull, not the default. - Do not quote Boolean property values (for example, use
truenot"true").
For a complete reference to all the Settings properties, see the SDK object reference: Settings.
| Property | Description |
|---|---|
version | Settings format version (integer). The default and only supported value is 1. |
builder | Configuration for Builder. |
cawg_trust | Configuration for CAWG trust lists. |
cawg_x509_signer | Configuration for the CAWG x.509 signer. |
core | Configuration for core features. |
signer | Configuration for the base C2PA signer. |
trust | Configuration for C2PA trust lists. |
verify | Configuration 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.
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:
- Create separate contexts for different threads
- Use
Arc<Context>to share a context across threads (for read-only access) - 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:
| Aspect | Old Thread-Local Settings | New Context API |
|---|---|---|
| Scope | Global, affects all operations | Per-operation, explicitly passed |
| Thread Safety | Not thread-safe | Thread-safe, shareable with Arc |
| Configuration | Set once per thread | Can have multiple configurations |
| Testability | Difficult (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:
-
Create signers automatically: When you call
context.signer()orbuilder.save_to_stream(), theContextcreates a signer from thesignerfield inSettings(if present). -
Configure HTTP resolvers: The
Contextcreates default HTTP resolvers (for fetching remote manifests) and applies thecore.allowed_network_hostssetting fromSettings. -
Control verification: The
verifysettings control how manifests are validated. -
Configure builder behavior: The
buildersettings control thumbnail generation, actions, and other manifest creation options.
The Settings format hasn't changed; only how you provide those settings has changed.