---
title: "Media Signing Library API integration"
description: "Integrate the DigiCert Media Signing Library with Content Trust Manager to sign media with C2PA Content Credentials."
source_url: https://dev.digicert.com/content-trust-api/media-signing-library-api-integration.html
---
The DigiCert Media Signing Library (MSL) lets your application sign supported media through DigiCert ONE Content Trust Manager and embed C2PA Content Credentials.
## Overview
MSL signs supported media through DigiCert ONE Content Trust Manager and embeds C2PA Content Credentials in the resulting asset.
MSL is implemented in Rust and distributed as a platform-native dynamic library. Applications can use the Rust wrapper crate or call the C-compatible interface from C, Python, Node.js, Go, Java, or another FFI-capable language.
This guide explains standard signing, CAWG identity signing, request metadata, result handling, verification, and integration from Rust and widely used backend programming languages.
For the HTTP endpoint, request fields, and response format that the library uses, see [Media Signing API](https://dev.digicert.com/md/content-trust-api/media-signing-api.md).
## Supported media types
The following media types are supported by the MSL.
|Category|Supported media types|
|---|---|
|Still image|image/jpeg, image/png, image/x-adobe-dng, image/heic, image/heif, image/tiff, image/webp,
image/svg+xml, image/gif, image/jxl|
|Video|video/mp4, video/x-msvideo, video/quicktime|
|Audio|audio/wav, audio/mp4, audio/flac, audio/mpeg|
|Document|application/pdf|
> **Warning**
>
> Never embed API keys, credential PINs, or other secrets in source code.
## Architecture and integration choices
|Surface|Best use|Result|
|---|---|---|
|Rust wrapper|Rust applications needing request validation
and path helpers|SignImageResponse|
|Path-based C FFI|Backend services and large assets|Writes output and returns output_path|
|In-memory C FFI|Byte-oriented pipelines|Returns signed_data and signed_data_len|
Prefer path-based signing for large assets because it avoids copying the complete signed file across the language boundary.
### 1.1 Platform libraries
|Operating system|Binary|Typical path|
|---|---|---|
|macOS|libc2pa_rust.dylib|rust_binary/libc2pa_rust.dylib|
|Linux|libc2pa_rust.so|rust_binary/libc2pa_rust.so|
|Windows|c2pa_rust.dll|rust_binary/c2pa_rust.dll|
```
msl-app/
├── include/c2pa_msl.h
├── input/
├── output/
└── rust_binary/
```
## Prerequisites and configuration
Obtain an account ID, API key, and signing-service URL from the same Content Trust Manager
environment(production: `one.digicert.com`; demo: `demo.one.digicert.com`). CAWG identity signing additionally needs an S/MIME credential ID and PIN.
```
export MSL_ACCOUNT_ID=""
export MSL_API_KEY=""
export MSL_SIGNING_URL="https://one.digicert.com/documentmanager"
export C2PA_SIGNING_SERVICE_URL="$MSL_SIGNING_URL"
export C2PA_API_KEY="$MSL_API_KEY"
export C2PA_SKIP_SSL_VALIDATION="false"
export SKIP_C2PA_PUBLIC_TRUST_LIST_CHECK="false"
export C2PA_CLAIM_GENERATOR_NAME="DigiCert Content Trust Manager"
```
> **Important**
>
> Use an account ID, API key, and signing-service URL from the same environment. Keep TLS validation enabled in production.
### 2.1 Check the native library
```
file rust_binary/libc2pa_rust.dylib
shasum -a 256 rust_binary/libc2pa_rust.dylib
```
> **Warning**
>
> Remove a browser quarantine attribute only after verifying the library source and checksum.
If Gatekeeper blocks the verified library, macOS users can run:
```
xattr -d com.apple.quarantine rust_binary/libc2pa_rust.dylib
```
### 2.2 Start-to-finish Python quickstart
This walkthrough starts with the MSL download and finishes with a verified signed image. Python can
call the C-compatible interface using its built-in `ctypes` module, so no additional Python package is required.
#### Step 1: Download the library
1. Sign in to DigiCert ONE Content Trust Manager.
2. Open **Client Tool repository** in the Content Trust Manager menu.
3. Download the Media Signing Library package for the operating system and processor architecture used by the application:
- macOS: `libc2pa_rust.dylib`
- Linux: `libc2pa_rust.so`
- Windows: `c2pa_rust.dll`
4. Keep the accompanying `c2pa_msl.h` header (see Appendix A) with the integration project. Python does not compile this header, but it provides the authoritative C structure and function declarations.
#### Step 2: Create the project folders
Open Terminal, PowerShell, or the integrated terminal in Visual Studio Code and run:
```
mkdir msl-python-quickstart
cd msl-python-quickstart
mkdir input output rust_binary include
```
The project should have this layout:
```
msl-python-quickstart/
├── include/c2pa_msl.h
├── input/sample.jpeg
├── output/
├── rust_binary/
└── sign_media.py
```
Copy the downloaded library into `rust_binary` , copy `c2pa_msl.h` into `include` , and place an unsigned supported image at `input/sample.jpeg` .
#### Step 3: Configure credentials
Set credentials in the terminal session rather than writing them into source code.
macOS or Linux:
```
export MSL_ACCOUNT_ID=""
export MSL_API_KEY=""
export MSL_SIGNING_URL="https://one.digicert.com/documentmanager"
```
Windows PowerShell:
```
$env:MSL_ACCOUNT_ID=""
$env:MSL_API_KEY=""
$env:MSL_SIGNING_URL="https://one.digicert.com/documentmanager"
```
#### Step 4: Create `sign_media.py`
Create `sign_media.py` in the project root and add the following program:
```
import ctypes
import os
import platform
from pathlib import Path
```
```
class C2paSignedResult(ctypes.Structure):
_fields_ = [
("signed_data", ctypes.c_void_p),
("signed_data_len", ctypes.c_size_t),
("manifest_id", ctypes.c_void_p),
("manifest_json", ctypes.c_void_p),
("error_message", ctypes.c_void_p),
("error_code", ctypes.c_int32),
("output_path", ctypes.c_void_p),
]
library_name = {
"Darwin": "libc2pa_rust.dylib",
"Linux": "libc2pa_rust.so",
"Windows": "c2pa_rust.dll",
}[platform.system()]
os.environ["C2PA_SIGNING_SERVICE_URL"] = os.environ["MSL_SIGNING_URL"]
os.environ["C2PA_API_KEY"] = os.environ["MSL_API_KEY"]
os.environ["C2PA_SKIP_SSL_VALIDATION"] ="false"
os.environ["SKIP_C2PA_PUBLIC_TRUST_LIST_CHECK"] ="false"
os.environ["C2PA_CLAIM_GENERATOR_NAME"] ="DigiCert Content Trust Manager"
library_path = Path("rust_binary", library_name).resolve()
library = ctypes.CDLL(str(library_path))
sign = library.c2pa_sign_content_from_path
sign.argtypes = [ctypes.c_char_p] *10
sign.restype = ctypes.POINTER(C2paSignedResult)
free_result = library.c2pa_free_result
free_result.argtypes = [ctypes.POINTER(C2paSignedResult)]
result_pointer = sign(
b"input/sample.jpeg",
b"output/sample_signed.jpeg",
b"sample.jpeg",
b"creator",
b"http://timestamp.digicert.com",
os.environ["MSL_ACCOUNT_ID"].encode(),
None, None, None, None,
)
if not result_pointer:
raise RuntimeError("MSL returned a null result pointer")
try:
result = result_pointer.contents
if result.error_code != 0:
message = (ctypes.string_at(result.error_message).decode()
if result.error_message else "Unknown signing error")
raise RuntimeError(f"{result.error_code}: {message}")
signed_path = ctypes.string_at(result.output_path).decode()
manifest_id = ctypes.string_at(result.manifest_id).decode()
print(f"Signed file: {signed_path}")
print(f"Manifest ID: {manifest_id}")
finally:
free_result(result_pointer)
```
#### Step 5: Run the program
Ensure that `output/sample_signed.jpeg` does not already exist, and then run:
```
python3 sign_media.py
```
On Windows, use `python sign_media.py` if that is the configured Python command. A successful request writes `output/sample_signed.jpeg` and prints its manifest ID.
#### Step 6: Verify the signed asset
Install `c2patool` separately if it is not already available, and run:
```
c2patool --detailed output/sample_signed.jpeg
```
Confirm that the output includes a valid active manifest and `"validation_state": "Valid"` . See Chapter 12 for additional verification guidance.
## Native C FFI
The `c2pa_msl.h` header (see Appendix A) defines the public standard-signing interface.
### 3.1 Result structure
```
typedef struct{
uint8_t *signed_data;
size_t signed_data_len;
char *manifest_id;
char *manifest_json;
char *error_message;
int32_t error_code;
char *output_path;
} C2paSignedResult;
```
|Field|In-memory success|Path success|Error|
|---|---|---|---|
|signed_data|Signed bytes|NULL|NULL|
|signed_data_len|Byte count|0|0|
|manifest_id|Present|Present|NULL|
|manifest_json|Present|Present|NULL|
|error_message|NULL|NULL|Present when available|
|error_code|0|0|Non-zero|
|output_path|NULL|Present|NULL|
`manifest_json` is a UTF-8 JSON representation of the generated manifest. Applications can parse it to inspect manifest properties such as assertions, claim-generator information, ingredients, signature information, the manifest label, and the asset title.
### 3.2 Signing functions
```
C2paSignedResult *c2pa_sign_content(
const uint8_t *file_data,
size_t file_data_len,
const char *filename,
const char *roles_csv,
const char *tsa_url,
const char *account_id,
const char *user_id,
```
```
const char *additional_actions_json,
const char *signing_metadata_json,
const char *trace_id
);
C2paSignedResult *c2pa_sign_content_from_path(
const char *input_path,
const char *output_path,
const char *filename,
const char *roles_csv,
const char *tsa_url,
const char *account_id,
const char *user_id,
const char *additional_actions_json,
const char *signing_metadata_json,
const char *trace_id
);
```
For path-based signing, input path, output path, filename, roles, timestamp authority (TSA) URL, and account ID must contain valid non-empty values. For in-memory signing, the input byte pointer and length are also required. User ID, additional actions, signing metadata, and trace ID are optional and may be `NULL` .
> **Important**
>
> Path-based signing does not overwrite an existing destination.
### 3.3 Ownership
```
void c2pa_free_result(C2paSignedResult *result);
```
> **Important**
>
> Call `c2pa_free_result` exactly once for each result. Do not free individual fields or use them after release.
1. Check whether the returned pointer is `NULL`.
1. Read `error_code`.
1. Copy strings or bytes required by the application.
1. Call `c2pa_free_result` exactly once.
c2pa_free_result accepts NULL.
## Roles, actions, and metadata
### 4.1 Standard roles
Standard signing accepts creator, contributor, and publisher as comma-separated lowercase values.
### 4.2 Additional actions
The top level must be one JSON object, not an array. Callers can supply c2pa.edited, c2pa.resized, and c2pa.filtered. c2pa.created and c2pa.opened are managed by the library.
```
{
"c2pa.edited":{
"software":"Example Editor",
"version":"1.0",
"time":"2026-08-21T06:00:00Z"
```
```
},
"c2pa.resized":{
"software":"Example Resizer",
"version":"2.0",
"time":"2026-08-21T06:01:00Z"
}
}
```
### 4.3 Signing metadata
```
{
"isNewCreation":true,
"createdWithAi":true,
"digitalSourceType":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture",
"includeExifMetadata":true,
"aiInference":"constrained",
"aiInferenceConstraintsInfo":"Permitted only for internal evaluation.",
"generativeAiTraining":"constrained",
"generativeAiTrainingConstraintsInfo":"Requires written permission.",
"dataMiningAndAnalytics":"notAllowed",
"nonGenerativeAiTraining":"allowed",
"externalReference":{
"location":{
"uri":"https://www.example.com/resource",
"contentType":"application/json"
}
}
}
```
A matching non-empty ConstraintsInfo field is required whenever a permission is constrained.
createdWithAi for new content results in trainedAlgorithmicMedia. editedWithAi results in compositedWithTrainedAlgorithmicMedia for the edited action.
The external-reference URI must be absolute and include a scheme. MSL records but does not retrieve it.
### 4.4 EXIF
When includeExifMetadata is true, supported EXIF properties are copied to the c2pa.metadata gathered assertion. When false or omitted, c2pa.metadata is absent.
EXIF metadata copying applies to the supported image formats documented for your MSL release.
Only EXIF properties permitted by C2PA 2.4 Appendix B, Table 18 should be included.
### 4.5 Trace ID
Pass a UUID v4 trace ID for correlation. The library generates one when it is omitted.
## Rust wrapper
```
cargo init
cargo add c2pa-rust-digicert-example@=1.1.1 anyhow
```
### 5.1 Example A: mandatory fields
This example supplies the values required by `with_defaults` , selects path-based standard signing, and keeps TLS certificate validation enabled.
```
use anyhow::Result;
use c2pa_rust_digicert_example::{
sign_image_file, SignImageRequest, SigningImplementation,
};
fn main() -> Result<()>{
let mut request = SignImageRequest::with_defaults(
"input/sample.jpeg",
"output",
"rust_binary/libc2pa_rust.dylib",
std::env::var("MSL_ACCOUNT_ID")?,
std::env::var("MSL_API_KEY")?,
std::env::var("MSL_SIGNING_URL")?,
);
request.signing_implementation =
SigningImplementation::StandardFromPath;
request.skip_ssl_validation =false;
let response = sign_image_file(request)?;
println!("Signed file: {}", response.output_path.display());
println!("Output size: {} bytes", response.output_size);
ifletSome(id) = response.manifest_id {
println!("Manifest ID: {id}");
}
Ok(())
}
```
### 5.2 Example B: all standard-signing fields
This example sets every field applicable to standard path-based signing. Export `MSL_USER_ID` if userlevel attribution is required.
```
use anyhow::Result;
use c2pa_rust_digicert_example::{
sign_image_file, SignImageRequest, SigningImplementation,
};
fn main() -> Result<()>{
let mut request = SignImageRequest::with_defaults(
"input/sample.jpeg",
"output",
"rust_binary/libc2pa_rust.dylib",
std::env::var("MSL_ACCOUNT_ID")?,
std::env::var("MSL_API_KEY")?,
std::env::var("MSL_SIGNING_URL")?,
);
request.user_id =std::env::var("MSL_USER_ID")?;
request.roles_csv ="creator,contributor,publisher".to_string();
request.tsa_url ="http://timestamp.digicert.com".to_string();
request.signing_implementation =
SigningImplementation::StandardFromPath;
request.skip_ssl_validation =false;
request.skip_c2pa_public_trust_list_check =false;
```
```
request.additional_actions_json =Some(r#"{
"c2pa.edited": {
"software": "Example Editor", "version": "1.0",
"time": "2026-08-21T06:00:00Z"
},
"c2pa.resized": {
"software": "Example Resizer", "version": "2.0",
"time": "2026-08-21T06:01:00Z"
}
}"#.to_string());
request.signing_metadata_json =Some(r#"{
"isNewCreation": true,
"createdWithAi": true,
"editedWithAi": false,
"digitalSourceType":
"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
"includeExifMetadata": true,
"aiInference": "constrained",
"aiInferenceConstraintsInfo": "Internal evaluation only.",
"generativeAiTraining": "constrained",
"generativeAiTrainingConstraintsInfo": "Written permission required.",
"dataMiningAndAnalytics": "constrained",
"dataMiningAndAnalyticsConstraintsInfo": "Contract terms apply.",
"nonGenerativeAiTraining": "constrained",
"nonGenerativeAiTrainingConstraintsInfo": "Written permission required.",
"externalReference": {
"location": {
"uri": "https://www.example.com/resource",
"contentType": "application/json"
}
}
}"#.to_string());
request.trace_id =
Some("550e8400-e29b-41d4-a716-446655440000".to_string());
let response = sign_image_file(request)?;
println!("Signed file: {}", response.output_path.display());
println!("Output size: {} bytes", response.output_size);
ifletSome(id) = response.manifest_id {
println!("Manifest ID: {id}");
}
Ok(())
}
```
Modes are `Standard` , `StandardFromPath` , `CawgIdentity` , and `CawgIdentityFromPath` . The path-based modes read the source asset directly from disk and write the signed asset directly to disk. They are available when the platform-specific MSL binary ( `.dylib` , `.so` , or `.dll` ) is version 1.1.0 or later.
## C example
### 6.1 Example A: mandatory fields
```
#include
#include
#include "include/c2pa_msl.h"
int main(void){
C2paSignedResult *r = c2pa_sign_content_from_path(
"input/sample.jpeg","output/c_signed.jpeg","sample.jpeg",
"creator","http://timestamp.digicert.com",
getenv("MSL_ACCOUNT_ID"), NULL, NULL, NULL, NULL);
if(r == NULL)return1;
int status =0;
if(r->error_code != 0){
fprintf(stderr,"%d: %s\n", r->error_code,
r->error_message ? r->error_message :"Unknown error");
status =1;
}else{
printf("Signed file: %s\n", r->output_path);
printf("Manifest ID: %s\n", r->manifest_id);
}
c2pa_free_result(r);
return status;
}
clang-Wall-Wextra-Iinclude c_msl.c \
-Lrust_binary-lc2pa_rust\
-Wl,-rpath,@executable_path/rust_binary -o c_msl
./c_msl
```
### 6.2 Example B: all standard-signing fields
```
#include
#include
#include "include/c2pa_msl.h"
int main(void){
const char *actions =
"{\"c2pa.edited\":{\"software\":\"Example Editor\","
"\"version\":\"1.0\",\"time\":\"2026-08-21T06:00:00Z\"}}";
const char *metadata =
"{\"isNewCreation\":true,\"createdWithAi\":true,"
"\"editedWithAi\":false,"
"\"digitalSourceType\":"
"\"http://cv.iptc.org/newscodes/digitalsourcetype/"
"trainedAlgorithmicMedia\","
"\"includeExifMetadata\":true,"
"\"aiInference\":\"constrained\","
"\"aiInferenceConstraintsInfo\":\"Internal evaluation only.\","
"\"generativeAiTraining\":\"constrained\","
"\"generativeAiTrainingConstraintsInfo\":"
"\"Written permission required.\","
"\"dataMiningAndAnalytics\":\"constrained\","
"\"dataMiningAndAnalyticsConstraintsInfo\":\"Contract terms apply.\","
"\"nonGenerativeAiTraining\":\"constrained\","
"\"nonGenerativeAiTrainingConstraintsInfo\":"
"\"Written permission required.\","
"\"externalReference\":{\"location\":{"
"\"uri\":\"https://www.example.com/resource\","
"\"contentType\":\"application/json\"}}}";
C2paSignedResult *r = c2pa_sign_content_from_path(
"input/sample.jpeg",
```
```
"output/c_all_fields_signed.jpeg",
"sample.jpeg",
"creator,contributor,publisher",
"http://timestamp.digicert.com",
getenv("MSL_ACCOUNT_ID"),
getenv("MSL_USER_ID"),
actions,
metadata,
"550e8400-e29b-41d4-a716-446655440000");
if(r == NULL){
fprintf(stderr,"MSL returned NULL\n");
return1;
}
int status =0;
if(r->error_code != 0){
fprintf(stderr,"%d: %s\n", r->error_code,
r->error_message ? r->error_message :"Unknown error");
status =1;
}else{
printf("Signed file: %s\n", r->output_path);
printf("Manifest ID: %s\n", r->manifest_id);
}
c2pa_free_result(r);
return status;
}
```
Compile this example in the same way, replacing `c_msl.c` with the filename used for the all-fields example.
## Python example
### 7.1 Example A: mandatory fields
```
import ctypes
import os
from pathlib import Path
class Result(ctypes.Structure):
_fields_ = [
("signed_data", ctypes.c_void_p),
("signed_data_len", ctypes.c_size_t),
("manifest_id", ctypes.c_void_p),
("manifest_json", ctypes.c_void_p),
("error_message", ctypes.c_void_p),
("error_code", ctypes.c_int32),
("output_path", ctypes.c_void_p),
]
os.environ["C2PA_SIGNING_SERVICE_URL"] = os.environ["MSL_SIGNING_URL"]
os.environ["C2PA_API_KEY"] = os.environ["MSL_API_KEY"]
os.environ["C2PA_SKIP_SSL_VALIDATION"] ="false"
lib = ctypes.CDLL(str(Path("rust_binary/libc2pa_rust.dylib").resolve()))
sign = lib.c2pa_sign_content_from_path
sign.argtypes = [ctypes.c_char_p] *10
sign.restype = ctypes.POINTER(Result)
free_result = lib.c2pa_free_result
free_result.argtypes = [ctypes.POINTER(Result)]
p = sign(
b"input/sample.jpeg", b"output/python_signed.jpeg", b"sample.jpeg",
b"creator", b"http://timestamp.digicert.com",
os.environ["MSL_ACCOUNT_ID"].encode(),
None, None, None, None)
if not p:
raise RuntimeError("MSL returned NULL")
try:
result = p.contents
if result.error_code:
message = (ctypes.string_at(result.error_message).decode()
if result.error_message else "Unknown error")
raise RuntimeError(f"{result.error_code}: {message}")
print(ctypes.string_at(result.output_path).decode())
finally:
free_result(p)
```
### 7.2 Example B: all standard-signing fields
```
import ctypes
import json
import os
from pathlib import Path
class Result(ctypes.Structure):
_fields_ = [
("signed_data", ctypes.c_void_p),
("signed_data_len", ctypes.c_size_t),
("manifest_id", ctypes.c_void_p),
("manifest_json", ctypes.c_void_p),
("error_message", ctypes.c_void_p),
("error_code", ctypes.c_int32),
("output_path", ctypes.c_void_p),
]
os.environ["C2PA_SIGNING_SERVICE_URL"] = os.environ["MSL_SIGNING_URL"]
os.environ["C2PA_API_KEY"] = os.environ["MSL_API_KEY"]
```
```
os.environ["C2PA_SKIP_SSL_VALIDATION"] ="false"
os.environ["SKIP_C2PA_PUBLIC_TRUST_LIST_CHECK"] ="false"
os.environ["C2PA_CLAIM_GENERATOR_NAME"] ="DigiCert Content Trust Manager"
actions = json.dumps({
"c2pa.edited": {
"software": "Example Editor",
"version": "1.0",
"time": "2026-08-21T06:00:00Z",
},
"c2pa.resized": {
"software": "Example Resizer",
"version": "2.0",
"time": "2026-08-21T06:01:00Z",
},
}).encode()
metadata = json.dumps({
"isNewCreation": True,
"createdWithAi": True,
"editedWithAi": False,
"digitalSourceType": (
"http://cv.iptc.org/newscodes/digitalsourcetype/"
"trainedAlgorithmicMedia"
),
"includeExifMetadata": True,
"aiInference": "constrained",
"aiInferenceConstraintsInfo": "Internal evaluation only.",
"generativeAiTraining": "constrained",
"generativeAiTrainingConstraintsInfo": "Written permission required.",
"dataMiningAndAnalytics": "constrained",
"dataMiningAndAnalyticsConstraintsInfo": "Contract terms apply.",
"nonGenerativeAiTraining": "constrained",
"nonGenerativeAiTrainingConstraintsInfo": "Written permission required.",
"externalReference": {
"location": {
"uri": "https://www.example.com/resource",
"contentType": "application/json",
}
},
}).encode()
lib = ctypes.CDLL(str(Path("rust_binary/libc2pa_rust.dylib").resolve()))
sign = lib.c2pa_sign_content_from_path
sign.argtypes = [ctypes.c_char_p] *10
sign.restype = ctypes.POINTER(Result)
free_result = lib.c2pa_free_result
free_result.argtypes = [ctypes.POINTER(Result)]
p = sign(
b"input/sample.jpeg",
b"output/python_all_fields_signed.jpeg",
b"sample.jpeg",
b"creator,contributor,publisher",
b"http://timestamp.digicert.com",
os.environ["MSL_ACCOUNT_ID"].encode(),
os.environ["MSL_USER_ID"].encode(),
actions,
metadata,
b"550e8400-e29b-41d4-a716-446655440000",
)
if not p:
raise RuntimeError("MSL returned NULL")
try:
result = p.contents
if result.error_code:
message = (ctypes.string_at(result.error_message).decode()
if result.error_message else "Unknown error")
raise RuntimeError(f"{result.error_code}: {message}")
print("Signed file:", ctypes.string_at(result.output_path).decode())
print("Manifest ID:", ctypes.string_at(result.manifest_id).decode())
manifest = json.loads(ctypes.string_at(result.manifest_json).decode())
print("Manifest title:", manifest.get("title"))
```
```
finally:
free_result(p)
```
## Node.js example
Install Koffi 3.1.6 or later:
```
npm install koffi
```
### 8.1 Example A: mandatory fields
```
const koffi =require("koffi");
const path =require("path");
process.env.C2PA_SIGNING_SERVICE_URL=process.env.MSL_SIGNING_URL;
process.env.C2PA_API_KEY=process.env.MSL_API_KEY;
process.env.C2PA_SKIP_SSL_VALIDATION="false";
const Result = koffi.struct("C2paSignedResult", {
signed_data:"void *",signed_data_len:"size_t",
manifest_id:"char *",manifest_json:"char *",
error_message:"char *",error_code:"int32_t",
output_path:"char *"
});
const lib = koffi.load(path.resolve("rust_binary/libc2pa_rust.dylib"));
const ResultPtr = koffi.pointer(Result);
const sign = lib.func("c2pa_sign_content_from_path", ResultPtr,
["str","str","str","str","str","str","str","str","str","str"]);
const release = lib.func("c2pa_free_result","void", [ResultPtr]);
const p =sign("input/sample.jpeg","output/node_signed.jpeg",
"sample.jpeg","creator","http://timestamp.digicert.com",
process.env.MSL_ACCOUNT_ID,null,null,null,null);
```
```
if (!p) thrownewError("MSL returned NULL");
try {
const r = koffi.decode(p, Result);
if (r.error_code!==0) {
thrownewError(String(r.error_code) +": "+ r.error_message);
}
console.log("Signed file:", r.output_path);
console.log("Manifest ID:", r.manifest_id);
} finally {
release(p);
}
```
### 8.2 Example B: all standard-signing fields
```
const koffi =require("koffi");
const path =require("path");
process.env.C2PA_SIGNING_SERVICE_URL=process.env.MSL_SIGNING_URL;
process.env.C2PA_API_KEY=process.env.MSL_API_KEY;
process.env.C2PA_SKIP_SSL_VALIDATION="false";
process.env.SKIP_C2PA_PUBLIC_TRUST_LIST_CHECK="false";
process.env.C2PA_CLAIM_GENERATOR_NAME=
"DigiCert Content Trust Manager";
const Result = koffi.struct("C2paSignedResult", {
signed_data:"void *",signed_data_len:"size_t",
manifest_id:"char *",manifest_json:"char *",
error_message:"char *",error_code:"int32_t",
output_path:"char *"
});
const lib = koffi.load(path.resolve("rust_binary/libc2pa_rust.dylib"));
```
```
const ResultPtr = koffi.pointer(Result);
const sign = lib.func("c2pa_sign_content_from_path", ResultPtr,
["str","str","str","str","str","str","str","str","str","str"]);
const release = lib.func("c2pa_free_result","void", [ResultPtr]);
const actions =JSON.stringify({
"c2pa.edited": {
software:"Example Editor",version:"1.0",
time:"2026-08-21T06:00:00Z"
},
"c2pa.resized": {
software:"Example Resizer",version:"2.0",
time:"2026-08-21T06:01:00Z"
}
});
const metadata =JSON.stringify({
isNewCreation:true,
createdWithAi:true,
editedWithAi:false,
digitalSourceType:
"http://cv.iptc.org/newscodes/digitalsourcetype/"+
"trainedAlgorithmicMedia",
includeExifMetadata:true,
aiInference:"constrained",
aiInferenceConstraintsInfo:"Internal evaluation only.",
generativeAiTraining:"constrained",
generativeAiTrainingConstraintsInfo:"Written permission required.",
dataMiningAndAnalytics:"constrained",
dataMiningAndAnalyticsConstraintsInfo:"Contract terms apply.",
nonGenerativeAiTraining:"constrained",
nonGenerativeAiTrainingConstraintsInfo:"Written permission required.",
externalReference: {
location: {
uri:"https://www.example.com/resource",
contentType:"application/json"
}
}
});
const p =sign(
"input/sample.jpeg",
"output/node_all_fields_signed.jpeg",
"sample.jpeg",
"creator,contributor,publisher",
"http://timestamp.digicert.com",
process.env.MSL_ACCOUNT_ID,
process.env.MSL_USER_ID,
actions,
metadata,
"550e8400-e29b-41d4-a716-446655440000"
);
if (!p) thrownewError("MSL returned NULL");
try {
const r = koffi.decode(p, Result);
if (r.error_code!==0) {
thrownewError(String(r.error_code) +": "+
(r.error_message||"Unknown error"));
}
console.log("Signed file:", r.output_path);
console.log("Manifest ID:", r.manifest_id);
console.log("Manifest title:",JSON.parse(r.manifest_json).title);
} finally {
release(p);
}
```
## Go example
### 9.1 Example A: mandatory fields
```
package main
/*
#cgo CFLAGS: -I${SRCDIR}/include
#cgo darwin LDFLAGS: -L${SRCDIR}/rust_binary -lc2pa_rust_go
#include
#include "c2pa_msl.h"
*/
import"C"
import(
"fmt"
"os"
"unsafe"
)
func cs(v string)*C.char {return C.CString(v)}
func main(){
os.Setenv("C2PA_SIGNING_SERVICE_URL", os.Getenv("MSL_SIGNING_URL"))
os.Setenv("C2PA_API_KEY", os.Getenv("MSL_API_KEY"))
os.Setenv("C2PA_SKIP_SSL_VALIDATION","false")
os.Setenv("SKIP_C2PA_PUBLIC_TRUST_LIST_CHECK","false")
input := cs("input/sample.jpeg")
output := cs("output/go_signed.jpeg")
filename := cs("sample.jpeg")
roles := cs("creator")
tsa := cs("http://timestamp.digicert.com")
account := cs(os.Getenv("MSL_ACCOUNT_ID"))
defer C.free(unsafe.Pointer(input))
defer C.free(unsafe.Pointer(output))
defer C.free(unsafe.Pointer(filename))
defer C.free(unsafe.Pointer(roles))
defer C.free(unsafe.Pointer(tsa))
defer C.free(unsafe.Pointer(account))
r := C.c2pa_sign_content_from_path(
input, output, filename, roles, tsa, account,
nil,nil,nil,nil)
if r ==nil{panic("MSL returned NULL")}
defer C.c2pa_free_result(r)
if r.error_code != 0{
panic(fmt.Sprintf("%d: %s",int32(r.error_code),
C.GoString(r.error_message)))
}
fmt.Println("Signed file:", C.GoString(r.output_path))
}
```
The Go example links to `libc2pa_rust_go.dylib` . When using a different filename, update the cgo linker flag accordingly.
### 9.2 Example B: all standard-signing fields
```
package main
/*
#cgo CFLAGS: -I${SRCDIR}/include
#cgo darwin LDFLAGS: -L${SRCDIR}/rust_binary -lc2pa_rust_go
#include
#include "c2pa_msl.h"
*/
```
```
import"C"
import(
"fmt"
"os"
"unsafe"
)
func cs(value string)*C.char {return C.CString(value)}
func main(){
os.Setenv("C2PA_SIGNING_SERVICE_URL", os.Getenv("MSL_SIGNING_URL"))
os.Setenv("C2PA_API_KEY", os.Getenv("MSL_API_KEY"))
os.Setenv("C2PA_SKIP_SSL_VALIDATION","false")
os.Setenv("SKIP_C2PA_PUBLIC_TRUST_LIST_CHECK","false")
os.Setenv("C2PA_CLAIM_GENERATOR_NAME","DigiCert Content Trust Manager")
actions :=`{
"c2pa.edited": {
"software": "Example Editor", "version": "1.0",
"time": "2026-08-21T06:00:00Z"
}
}`
metadata :=`{
"isNewCreation": true,
"createdWithAi": true,
"editedWithAi": false,
"digitalSourceType":
"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
"includeExifMetadata": true,
"aiInference": "constrained",
"aiInferenceConstraintsInfo": "Internal evaluation only.",
"generativeAiTraining": "constrained",
"generativeAiTrainingConstraintsInfo": "Written permission required.",
"dataMiningAndAnalytics": "constrained",
"dataMiningAndAnalyticsConstraintsInfo": "Contract terms apply.",
"nonGenerativeAiTraining": "constrained",
"nonGenerativeAiTrainingConstraintsInfo": "Written permission required.",
"externalReference": {
"location": {
"uri": "https://www.example.com/resource",
"contentType": "application/json"
}
}
}`
values :=[]string{
"input/sample.jpeg",
"output/go_all_fields_signed.jpeg",
"sample.jpeg",
"creator,contributor,publisher",
"http://timestamp.digicert.com",
os.Getenv("MSL_ACCOUNT_ID"),
os.Getenv("MSL_USER_ID"),
actions,
metadata,
"550e8400-e29b-41d4-a716-446655440000",
}
args :=make([]*C.char,len(values))
for i, value :=range values {
args[i]= cs(value)
defer C.free(unsafe.Pointer(args[i]))
}
result := C.c2pa_sign_content_from_path(
args[0], args[1], args[2], args[3], args[4],
args[5], args[6], args[7], args[8], args[9])
if result ==nil{panic("MSL returned NULL")}
defer C.c2pa_free_result(result)
if result.error_code != 0{
message :="Unknown error"
if result.error_message !=nil{
message = C.GoString(result.error_message)
}
panic(fmt.Sprintf("%d: %s",int32(result.error_code), message))
```
```
}
fmt.Println("Signed file:", C.GoString(result.output_path))
fmt.Println("Manifest ID:", C.GoString(result.manifest_id))
fmt.Println("Manifest JSON:", C.GoString(result.manifest_json))
}
```
## Java example
Use OpenJDK 21 and JNA 5.17.0 or a compatible version.
### 10.1 Example A: mandatory fields
```
importcom.sun.jna.*;
importjava.util.*;
```
```
publicfinalclass MslExample {
publicstaticfinalclass SizeT extends IntegerType {
publicSizeT(){super(Native.SIZE_T_SIZE);}
}
publicstaticfinalclassResultextends Structure {
public Pointer signed_data;
public SizeT signed_data_len;
public Pointer manifest_id;
public Pointer manifest_json;
public Pointer error_message;
publicint error_code;
public Pointer output_path;
protectedListgetFieldOrder(){
returnArrays.asList("signed_data","signed_data_len",
"manifest_id","manifest_json","error_message",
"error_code","output_path");
}
publicResult(Pointer p){super(p);read();}
}
publicinterface Msl extends Library {
Pointer c2pa_sign_content_from_path(String input,String output,
String filename,String roles,String tsa,String account,
String user,String actions,String metadata,String trace);
voidc2pa_free_result(Pointer result);
}
publicstaticvoidmain(String[] args){
Msl lib = Native.load("../rust_binary/libc2pa_rust.dylib", Msl.class);
Pointer p = lib.c2pa_sign_content_from_path(
"../input/sample.jpeg","../output/java_signed.jpeg",
"sample.jpeg","creator","http://timestamp.digicert.com",
System.getenv("MSL_ACCOUNT_ID"),null,null,null,null);
if(p ==null)thrownewIllegalStateException("MSL returned NULL");
try{
Result r =newResult(p);
if(r.error_code!=0){
String m = r.error_message==null?"Unknown error"
: r.error_message.getString(0);
thrownewIllegalStateException(r.error_code+": "+ m);
}
System.out.println("Signed file: "+ r.output_path.getString(0));
}finally{
lib.c2pa_free_result(p);
}
}
}
```
### 10.2 Example B: all standard-signing fields
```
importcom.sun.jna.*;
importjava.util.*;
publicfinalclass MslAllFieldsExample {
publicstaticfinalclass SizeT extends IntegerType {
publicSizeT(){super(Native.SIZE_T_SIZE);}
}
publicstaticfinalclassResultextends Structure {
public Pointer signed_data;
public SizeT signed_data_len;
public Pointer manifest_id;
public Pointer manifest_json;
public Pointer error_message;
publicint error_code;
public Pointer output_path;
protectedListgetFieldOrder(){
returnArrays.asList("signed_data","signed_data_len",
"manifest_id","manifest_json","error_message",
"error_code","output_path");
}
publicResult(Pointer pointer){super(pointer);read();}
}
publicinterface Msl extends Library {
Pointer c2pa_sign_content_from_path(String input,String output,
String filename,String roles,String tsa,String account,
String user,String actions,String metadata,String trace);
voidc2pa_free_result(Pointer result);
}
publicstaticvoidmain(String[] args){
String actions ="""
{
"c2pa.edited": {
"software": "Example Editor",
"version": "1.0",
"time": "2026-08-21T06:00:00Z"
}
}
""";
String metadata ="""
{
"isNewCreation": true,
"createdWithAi": true,
"editedWithAi": false,
"digitalSourceType":
"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
"includeExifMetadata": true,
"aiInference": "constrained",
"aiInferenceConstraintsInfo": "Internal evaluation only.",
"generativeAiTraining": "constrained",
"generativeAiTrainingConstraintsInfo": "Written permission required.",
"dataMiningAndAnalytics": "constrained",
"dataMiningAndAnalyticsConstraintsInfo": "Contract terms apply.",
"nonGenerativeAiTraining": "constrained",
"nonGenerativeAiTrainingConstraintsInfo": "Written permission required.",
"externalReference": {
"location": {
"uri": "https://www.example.com/resource",
"contentType": "application/json"
}
}
}
""";
Msl lib = Native.load(
"../rust_binary/libc2pa_rust.dylib", Msl.class);
Pointer pointer = lib.c2pa_sign_content_from_path(
"../input/sample.jpeg",
```
```
"../output/java_all_fields_signed.jpeg",
"sample.jpeg",
"creator,contributor,publisher",
"http://timestamp.digicert.com",
System.getenv("MSL_ACCOUNT_ID"),
System.getenv("MSL_USER_ID"),
actions,
metadata,
"550e8400-e29b-41d4-a716-446655440000");
if(pointer ==null){
thrownewIllegalStateException("MSL returned NULL");
}
try{
Result result =newResult(pointer);
if(result.error_code!=0){
String message = result.error_message==null
?"Unknown error": result.error_message.getString(0);
thrownewIllegalStateException(
result.error_code+": "+ message);
}
System.out.println(
"Signed file: "+ result.output_path.getString(0));
System.out.println(
"Manifest ID: "+ result.manifest_id.getString(0));
System.out.println(
"Manifest JSON: "+ result.manifest_json.getString(0));
}finally{
lib.c2pa_free_result(pointer);
}
}
}
```
## CAWG identity signing
CAWG identity signing adds verifiable identity information using an S/MIME Baseline Requirements credential.
> **Important**
>
> CAWG identity assertion signing is supported only in the production environment. Use an account ID, API key, service URL, and S/MIME credential from that same environment.
### 11.1 Requirements and modes
Required values are account ID, API key, S/MIME credential ID, credential PIN, and supported CAWG role.
|Mode|Behaviour|
|---|---|
|SigningImplementation::CawgIdentity|In-memory CAWG identity signing|
|SigningImplementation::CawgIdentityFromPath|Path-based CAWG identity signing|
`CawgIdentityFromPath` reads the source asset from disk and writes the signed asset directly to disk, reducing memory use for large files. This mode is available when the platform-specific MSL binary ( `.dylib` , `.so` , or `.dll` ) is version 1.1.0 or later. Rust wrapper output names use the `_cawg_signed` suffix.
### 11.2 CAWG roles
|Role|Identity field|
|---|---|
|creator|cawg.creator|
|publisher|cawg.publisher|
|contributor|cawg.contributor|
|editor|cawg.editor|
|producer|cawg.producer|
|sponsor|cawg.sponsor|
|translator|cawg.translator|
### 11.3 Rust CAWG example A: mandatory fields
This example adds the two CAWG-specific credential fields to the values required by `with_defaults` .
```
use anyhow::Result;
use c2pa_rust_digicert_example::{
sign_image_file, SignImageRequest, SigningImplementation,
};
fn main() -> Result<()>{
let mut request = SignImageRequest::with_defaults(
"input/sample.jpeg",
"output",
"rust_binary/libc2pa_rust.dylib",
std::env::var("MSL_ACCOUNT_ID")?,
std::env::var("MSL_API_KEY")?,
std::env::var("MSL_SIGNING_URL")?,
);
request.signing_implementation =
SigningImplementation::CawgIdentityFromPath;
request.smime_credential_id =
Some(std::env::var("MSL_SMIME_CREDENTIAL_ID")?);
request.user_pin =
Some(std::env::var("MSL_USER_PIN")?);
request.skip_ssl_validation =false;
request.skip_c2pa_public_trust_list_check =false;
let response = sign_image_file(request)?;
println!("Signed file: {}", response.output_path.display());
ifletSome(id) = response.manifest_id {
println!("Manifest ID: {id}");
}
Ok(())
}
```
### 11.4 Rust CAWG example B: all fields
```
use anyhow::Result;
use c2pa_rust_digicert_example::{
sign_image_file, SignImageRequest, SigningImplementation,
};
fn main() -> Result<()>{
let mut request = SignImageRequest::with_defaults(
"input/sample.jpeg",
```
```
"output",
"rust_binary/libc2pa_rust.dylib",
std::env::var("MSL_ACCOUNT_ID")?,
std::env::var("MSL_API_KEY")?,
std::env::var("MSL_SIGNING_URL")?,
);
request.user_id =std::env::var("MSL_USER_ID")?;
request.roles_csv =
"creator,publisher,contributor,editor,producer,sponsor,translator"
.to_string();
request.tsa_url ="http://timestamp.digicert.com".to_string();
request.signing_implementation =
SigningImplementation::CawgIdentityFromPath;
request.skip_ssl_validation =false;
request.skip_c2pa_public_trust_list_check =false;
request.additional_actions_json =Some(r#"{
"c2pa.edited": {
"software": "Example Editor", "version": "1.0",
"time": "2026-08-21T06:00:00Z"
}
}"#.to_string());
request.signing_metadata_json =Some(r#"{
"isNewCreation": false,
"createdWithAi": false,
"editedWithAi": true,
"digitalSourceType":
"http://cv.iptc.org/newscodes/digitalsourcetype/compositedWithTrainedAlgorithmicMedia",
"includeExifMetadata": true,
"aiInference": "constrained",
"aiInferenceConstraintsInfo": "Internal evaluation only.",
"generativeAiTraining": "constrained",
"generativeAiTrainingConstraintsInfo": "Written permission required.",
"dataMiningAndAnalytics": "constrained",
"dataMiningAndAnalyticsConstraintsInfo": "Contract terms apply.",
"nonGenerativeAiTraining": "constrained",
"nonGenerativeAiTrainingConstraintsInfo": "Written permission required.",
"externalReference": {
"location": {
"uri": "https://www.example.com/resource",
"contentType": "application/json"
}
}
}"#.to_string());
request.trace_id =
Some("550e8400-e29b-41d4-a716-446655440000".to_string());
request.smime_credential_id =
Some(std::env::var("MSL_SMIME_CREDENTIAL_ID")?);
request.user_pin =Some(std::env::var("MSL_USER_PIN")?);
let response = sign_image_file(request)?;
println!("Signed file: {}", response.output_path.display());
println!("Output size: {} bytes", response.output_size);
ifletSome(id) = response.manifest_id {
println!("Manifest ID: {id}");
}
Ok(())
}
```
Before signing, verify that credential ID, PIN, account ID, API key, and service URL belong to the same environment.
## Verification
```
c2patool --detailed output/sample_signed.jpeg
```
An internally consistent result reports:
```
"validation_state": "Valid"
```
Inspect the active manifest, assertions, signature, and validation results.
### 12.1 Tamper test
```
cp output/sample_signed.jpeg output/sample_signed_tampered.jpeg
printf'\0'>> output/sample_signed_tampered.jpeg
c2patool --detailed output/sample_signed_tampered.jpeg
```
A changed asset should report assertion.dataHash.mismatch and an Invalid state.
> **Note**
>
> `c2patool` might report `signingCredential.untrusted` or `timeStamp.untrusted` when local trust anchors are unavailable. Distinguish local trust configuration from hash or signature failures.
## Errors and troubleshooting
|Condition|Result|Handling|
|---|---|---|
|Invalid API key|5020 and HTTP 401 wrong_token|Correct credentials|
|Existing output|5022, destination exists|Use a new path or remove the old file|
|Invalid role|4001|Use roles allowed for the mode|
|Invalid JSON or metadata|4000|Correct JSON shape or conditional fields|
|Missing input or library|Local path or load error|Validate paths and architecture|
|Modified signed file|dataHash mismatch|Treat asset as changed|
### 13.1 macOS linking
macOS records the location of each dynamic library inside the executable that links to it. If that recorded location is an absolute path from another computer or from a build system, the application can compile successfully but fail when it starts because the path does not exist on the customer machine.
Use `otool` to inspect both sides of the link:
```
# Show the dynamic-library paths recorded in the application.
otool -L c_msl
# Show the install name stored in the MSL library.
otool -D rust_binary/libc2pa_rust.dylib
```
Portable paths normally begin with `@rpath` , `@loader_path` , or `@executable_path` . A path such as `/Users/runner/work/.../libc2pa_rust.dylib` refers to a build machine and will not normally exist on the system where the application is deployed.
If `otool -L c_msl` reports such an absolute path, replace it in the application with the packaged library location:
```
install_name_tool -change\
""\
"@executable_path/rust_binary/libc2pa_rust.dylib"\
c_msl
codesign --force--sign- c_msl
```
`@executable_path` means “start from the folder that contains the running executable.” In this example, macOS therefore looks for the MSL library in the executable's `rust_binary` subfolder.
> **Note**
>
> The `install_name_tool` command modifies the executable and invalidates its existing code signature. The `codesign` command above applies an ad-hoc signature for local development. For production distribution, use your organization’s standard macOS code-signing process and package the library with a portable install name so that this repair is unnecessary.
After making the change, run `otool -L c_msl` again and confirm that the old absolute path is no longer present.
#### Native log output
> **Tip**
>
> Avoid piping native log output directly to a command that stops reading after the first match, such as `head` or `grep -m 1`, because the receiving command closes the pipe while the signing process is still writing. Consume the complete stream or redirect it to a file.
MSL can write detailed native logs while signing:
```
./c_msl > msl.log 2>&1
```
Review the saved log after the signing process finishes:
```
grep"Signing Completed" msl.log
```
## Platform and tool compatibility
|Component|Version or platform|Integration notes|
|---|---|---|
|macOS library|Universal arm64 and x86_64|Use the .dylib package|
|Rust|Rust 1.89.0; wrapper 1.1.1|Use the Rust wrapper crate|
|Python|Python 3.9.6 or later|Use the standard ctypes module|
|C|Apple clang on arm64 or x86_64|Include c2pa_msl.h and link the native library|
|Node.js|Node.js 26.0.0; Koffi 3.1.6|Use Koffi to call the C-compatible interface|
|Go|Go 1.26.7 on darwin/arm64|Use cgo and the C header|
|Java|OpenJDK 21; JNA 5.17.0|Use JNA to call the C-compatible interface|
|c2patool|0.27.10|Use to inspect and verify signed assets|
## Appendix A. C interface header
The distributed header is the authoritative definition for standard C FFI integration.
```
#ifndef C2PA_MSL_H
#define C2PA_MSL_H
#include
#include
#ifdef __cplusplus
extern"C"{
#endif
typedef struct{
uint8_t *signed_data;
size_t signed_data_len;
char *manifest_id;
char *manifest_json;
char *error_message;
int32_t error_code;
char *output_path;
} C2paSignedResult;
C2paSignedResult *c2pa_sign_content(
const uint8_t *,size_t,const char *,const char *,
const char *,const char *,const char *,const char *,
const char *,const char *);
C2paSignedResult *c2pa_sign_content_from_path(
const char *,const char *,const char *,const char *,
const char *,const char *,const char *,const char *,
const char *,const char *);
void c2pa_free_result(C2paSignedResult *result);
#ifdef __cplusplus
}
#endif
#endif
```
## Appendix B. Release sequence
1. Call the signing function.
2. Check for a NULL result.
3. Read error_code.
4. Copy error_message on error.
5. Copy manifest ID, JSON, output path, or signed bytes on success.
6. Call c2pa_free_result exactly once.
7. Never use returned pointers after release.
## Appendix C. References
1. **Media Signing API:** [Media Signing API](https://dev.digicert.com/md/content-trust-api/media-signing-api.md).
1. **S/MIME BR certificate setup:** [Get an S/MIME BR certificate](https://docs.digicert.com/en/content-trust-manager/sign-media/set-up-your-account/get-an-s-mime-br-certificate.html).
1. **Digital Source Type vocabulary:** [IPTC NewsCodes](https://cv.iptc.org/newscodes/digitalsourcetype/).