Media Signing Library

Integrate the DigiCert Media Signing Library with Content Trust Manager to sign media with C2PA Content Credentials.

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.

Supported media types

The following media types are supported by the MSL.

CategorySupported media types
Still imageimage/jpeg, image/png, image/x-adobe-dng, image/heic, image/heif, image/tiff, image/webp,
image/svg+xml, image/gif, image/jxl
Videovideo/mp4, video/x-msvideo, video/quicktime
Audioaudio/wav, audio/mp4, audio/flac, audio/mpeg
Documentapplication/pdf
Security: Never embed API keys, credential PINs, or other secrets in source code.

Architecture and integration choices

SurfaceBest useResult
Rust wrapperRust applications needing request validation
and path helpers
SignImageResponse
Path-based C FFIBackend services and large assetsWrites output and returns output_path
In-memory C FFIByte-oriented pipelinesReturns 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 systemBinaryTypical path
macOSlibc2pa_rust.dylibrust_binary/libc2pa_rust.dylib
Linuxlibc2pa_rust.sorust_binary/libc2pa_rust.so
Windowsc2pa_rust.dllrust_binary/c2pa_rust.dll
msl-app/
├── include/c2pa_msl.h
├── input/
├── output/
└── rust_binary/<platform-library>

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="<account-id>"
export MSL_API_KEY="<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"
Use an account ID, API key, and signing-service URL from the same environment. Keep TLS validation enabled in production.

When MSL reads environment variables

MSL reads the following environment variables once, when the application starts. It uses those values until the application process ends. MSL does not read these variables again for each signing request.

Set these variables before you start the application. MSL does not use changes made while the application is running. To apply a change, update the environment used to start the application, then restart the process.

Environment variableWhen MSL reads itValue kept until the process ends?Restart needed to apply changes?
C2PA_API_KEYApplication startupYesYes
C2PA_SIGNING_SERVICE_URLApplication startupYesYes
C2PA_SKIP_SSL_VALIDATIONApplication startupYesYes
SKIP_C2PA_PUBLIC_TRUST_LIST_CHECKApplication startupYesYes
C2PA_CLAIM_GENERATOR_NAMEApplication startupYesYes
C2PA_CREDENTIAL_IDApplication startupYesYes

Restart required: Restart each application process that needs to use the new values.

Set the signing credential ID

Use C2PA_CREDENTIAL_ID to specify the signing credential ID. Set it before you start the application:

export C2PA_CREDENTIAL_ID="<credential-id>"

If you change the credential ID or other credential settings in the environment, restart the application to use the new values.

Update the API key used by MSL

To use a replacement API key, update the environment settings and restart the application:

  1. Set C2PA_API_KEY to the replacement key in the environment used to start the application.
  2. If your application copies the key from another setting, update that setting too. For example, this guide uses MSL_API_KEY to set C2PA_API_KEY.
  3. Restart every application process that uses the key. Start each process with the updated environment settings.
  4. Send a signing request from the restarted application. Check that authentication and signing succeed.

Until you restart the application, MSL continues to use the previous key. Signing can still succeed if that key is valid. If the previous key is revoked or becomes invalid, authentication fails.

Use a separate process for each account

Do not use one running application process to sign for multiple accounts. Changing environment variables does not switch the credentials MSL uses. New credential settings take effect only after the process restarts.

Use a separate application process for each account. Before starting each process, set its account ID, API key, signing-service URL, and credential settings for that account.

Passing a different account ID to a signing function does not change the API key or credential settings that MSL loaded at startup.

See When MSL reads environment variables.

2.1 Check the native library

file rust_binary/libc2pa_rust.dylib
shasum -a 256 rust_binary/libc2pa_rust.dylib
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/<platform-library>
└── 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 the credential environment variables in your terminal session before you start the Python application. Do not write secrets into source code.

MSL reads its environment variables once, when the application starts. It uses those values until the application process ends.

To change the API key or another MSL setting, update its environment variable. Then restart the Python application with the new values.

See When MSL reads environment variables and Update the API key used by MSL.

macOS or Linux:

export MSL_ACCOUNT_ID="<account-id>"
export MSL_API_KEY="<api-key>"
export MSL_SIGNING_URL="https://one.digicert.com/documentmanager"

Windows PowerShell:

$env:MSL_ACCOUNT_ID="<account-id>"
$env:MSL_API_KEY="<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 code below.

At startup, the code copies the API key from MSL_API_KEY into C2PA_API_KEY and the signing-service URL from MSL_SIGNING_URL into C2PA_SIGNING_SERVICE_URL. It also sets the validation options and claim-generator name.

Set these environment variables before calling ctypes.CDLL(...), which loads the native MSL library.

Changing these variables between signing requests does not change the values MSL uses. To apply new values, restart the application with the updated environment settings.

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()]

# Startup configuration: set before loading MSL.
# Changes after startup require restarting the application process.

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;
FieldIn-memory successPath successError
signed_dataSigned bytesNULLNULL
signed_data_lenByte count00
manifest_idPresentPresentNULL
manifest_jsonPresentPresentNULL
error_messageNULLNULLPresent when available
error_code00Non-zero
output_pathNULLPresentNULL

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 .

Path-based signing does not overwrite an existing destination.

3.3 Ownership

void c2pa_free_result(C2paSignedResult *result);
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.
  2. Read error_code.
  3. Copy strings or bytes required by the application.
  4. 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. They may also supply the optional digitalSourceType field, whose value must be a valid, fully qualified IPTC or C2PA Digital Source Type URI. c2pa.created and c2pa.opened are managed by the library.

{
  "c2pa.edited": {
    "software": "Example Editor",
    "version": "1.0",
    "time": "2026-08-21T06:00:00Z",
    "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
  },
  "c2pa.resized": {
    "software": "Example Resizer",
    "version": "2.0",
    "time": "2026-08-21T06:01:00Z",
    "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/humanEdits"
  }
}

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);
    if let Some(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 user-level 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);
    if let Some(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.

5.3 Example C: In-memory standard signing

Use the Standard implementation to select in-memory standard signing. The wrapper reads the source file, passes its bytes to the native in-memory function, and writes the returned signed bytes to the configured output directory.

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")?,
    );

    // Standard uses the in-memory native signing function.
    request.signing_implementation = SigningImplementation::Standard;
    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());
    println!("Output size: {} bytes", response.output_size);
    if let Some(id) = response.manifest_id {
        println!("Manifest ID: {id}");
    }
    Ok(())
}

C example

6.1 Example A: mandatory fields

#include <stdio.h>
#include <stdlib.h>
#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) return 1;

  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 <stdio.h>
#include <stdlib.h>
#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");
    return 1;
  }

  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.

6.3 Example C: In-memory standard signing

Use c2pa_sign_content when the application already holds the complete source asset in memory. The function returns the signed asset through signed_data and signed_data_len. Copy the signed bytes before calling c2pa_free_result.

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "include/c2pa_msl.h"

int main(void) {
  FILE *input = fopen("input/sample.jpeg", "rb");
  if (input == NULL) {
    perror("Unable to open input file");
    return 1;
  }

  if (fseek(input, 0, SEEK_END) != 0) {
    fclose(input);
    return 1;
  }
  long input_size = ftell(input);
  if (input_size <= 0 || fseek(input, 0, SEEK_SET) != 0) {
    fclose(input);
    return 1;
  }

  uint8_t *input_data = malloc((size_t)input_size);
  if (input_data == NULL) {
    fclose(input);
    return 1;
  }
  if (fread(input_data, 1, (size_t)input_size, input) !=
      (size_t)input_size) {
    free(input_data);
    fclose(input);
    return 1;
  }
  fclose(input);

  C2paSignedResult *result = c2pa_sign_content(
    input_data, (size_t)input_size, "sample.jpeg", "creator",
    "http://timestamp.digicert.com", getenv("MSL_ACCOUNT_ID"),
    NULL, NULL, NULL, NULL);
  free(input_data);

  if (result == NULL) {
    fprintf(stderr, "MSL returned NULL\n");
    return 1;
  }

  int status = 0;
  if (result->error_code != 0) {
    fprintf(stderr, "%d: %s\n", result->error_code,
      result->error_message ? result->error_message : "Unknown error");
    status = 1;
  } else {
    FILE *output = fopen("output/c_in_memory_signed.jpeg", "wb");
    if (output == NULL ||
        fwrite(result->signed_data, 1, result->signed_data_len, output) !=
          result->signed_data_len) {
      fprintf(stderr, "Unable to write signed output\n");
      status = 1;
    }
    if (output != NULL) fclose(output);
    if (status == 0) {
      printf("Manifest ID: %s\n", result->manifest_id);
    }
  }

  c2pa_free_result(result);
  return status;
}

The caller owns input_data. The returned signed_data remains owned by the result and must be copied before c2pa_free_result is called. Do not free individual result fields.

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)

7.3 Example C: In-memory standard signing

Use c2pa_sign_content when the application already holds the complete source asset in memory. The function returns the signed asset through signed_data and signed_data_len. Copy the signed bytes before calling c2pa_free_result.

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"
os.environ["SKIP_C2PA_PUBLIC_TRUST_LIST_CHECK"] = "false"

lib = ctypes.CDLL(str(Path("rust_binary/libc2pa_rust.dylib").resolve()))
sign = lib.c2pa_sign_content
sign.argtypes = [
    ctypes.c_void_p, ctypes.c_size_t,
    ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p,
    ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p,
    ctypes.c_char_p, ctypes.c_char_p,
]
sign.restype = ctypes.POINTER(Result)
free_result = lib.c2pa_free_result
free_result.argtypes = [ctypes.POINTER(Result)]
free_result.restype = None

source = Path("input/sample.jpeg").read_bytes()
source_buffer = ctypes.create_string_buffer(source)

pointer = sign(
    source_buffer, len(source), b"sample.jpeg", b"creator",
    b"http://timestamp.digicert.com",
    os.environ["MSL_ACCOUNT_ID"].encode(),
    None, None, None, None,
)

if not pointer:
    raise RuntimeError("MSL returned NULL")

try:
    result = pointer.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}")

    signed_bytes = ctypes.string_at(
        result.signed_data, result.signed_data_len)
    Path("output/python_in_memory_signed.jpeg").write_bytes(signed_bytes)
    print("Manifest ID:", ctypes.string_at(result.manifest_id).decode())
finally:
    free_result(pointer)

Copy signed_data before calling c2pa_free_result. ctypes.string_at performs that copy. The input buffer must remain alive until c2pa_sign_content returns.

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) throw new Error("MSL returned NULL");

try {
  const r = koffi.decode(p, Result);
  if (r.error_code !== 0) {
    throw new Error(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) throw new Error("MSL returned NULL");

try {
  const r = koffi.decode(p, Result);
  if (r.error_code !== 0) {
    throw new Error(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);
}

8.3 Example C: In-memory standard signing

Use c2pa_sign_content when the application already holds the complete source asset in memory. The function returns the signed asset through signed_data and signed_data_len. Copy the signed bytes before calling c2pa_free_result.

const fs = require("fs");
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";

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", ResultPtr, [
  "void *", "size_t", "str", "str", "str",
  "str", "str", "str", "str", "str"
]);
const release = lib.func("c2pa_free_result", "void", [ResultPtr]);

const source = fs.readFileSync("input/sample.jpeg");
const pointer = sign(
  source, source.length, "sample.jpeg", "creator",
  "http://timestamp.digicert.com", process.env.MSL_ACCOUNT_ID,
  null, null, null, null
);

if (!pointer) throw new Error("MSL returned NULL");

try {
  const result = koffi.decode(pointer, Result);
  if (result.error_code !== 0) {
    throw new Error(String(result.error_code) + ": " +
      (result.error_message || "Unknown error"));
  }

  const length = Number(result.signed_data_len);
  const signed = Buffer.from(
    koffi.decode(result.signed_data, "uint8_t", length));
  fs.writeFileSync("output/node_in_memory_signed.jpeg", signed);
  console.log("Manifest ID:", result.manifest_id);
} finally {
  release(pointer);
}

Copy the decoded signed bytes into a Node.js Buffer before calling c2pa_free_result. Keep the source buffer in scope until c2pa_sign_content returns. This example uses the same Koffi dependency and native-library path as the preceding Node.js examples.

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 <stdlib.h>
#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 <stdlib.h>
#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))
}

9.3 Example C: In-memory standard signing

Use c2pa_sign_content when the application already holds the complete source asset in memory. The function returns the signed bytes through signed_data and their length through signed_data_len. Copy these bytes before calling c2pa_free_result.

package main

/*
#cgo CFLAGS: -I${SRCDIR}/include
#cgo darwin LDFLAGS: -L${SRCDIR}/rust_binary -lc2pa_rust_go
#include <stdlib.h>
#include "c2pa_msl.h"
*/
import "C"

import (
    "fmt"
    "os"
    "unsafe"
)

func cString(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")

    source, err := os.ReadFile("input/sample.jpeg")
    if err != nil { panic(err) }
    if len(source) == 0 { panic("Input file is empty") }

    filename := cString("sample.jpeg")
    roles := cString("creator")
    tsa := cString("http://timestamp.digicert.com")
    account := cString(os.Getenv("MSL_ACCOUNT_ID"))
    defer C.free(unsafe.Pointer(filename))
    defer C.free(unsafe.Pointer(roles))
    defer C.free(unsafe.Pointer(tsa))
    defer C.free(unsafe.Pointer(account))

    result := C.c2pa_sign_content(
        (*C.uint8_t)(unsafe.Pointer(&source[0])), C.size_t(len(source)),
        filename, roles, tsa, account, nil, nil, nil, nil)
    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))
    }

    if result.signed_data_len > C.size_t(2147483647) {
        panic("Signed output is too large for this process")
    }
    signed := C.GoBytes(
        unsafe.Pointer(result.signed_data), C.int(result.signed_data_len))
    if err := os.WriteFile(
        "output/go_in_memory_signed.jpeg", signed, 0644); err != nil {
        panic(err)
    }
    fmt.Println("Manifest ID:", C.GoString(result.manifest_id))
}

In this Go example, C.GoBytes copies the signed bytes into a Go byte slice before the deferred c2pa_free_result call releases the native result. The empty-input check ensures that &source[0] is accessed only when the source contains at least one byte.

Java example

Use OpenJDK 21 and JNA 5.17.0 or a compatible version.

10.1 Example A: mandatory fields

import com.sun.jna.*;
import java.util.*;

public final class MslExample {
  public static final class SizeT extends IntegerType {
    public SizeT() { super(Native.SIZE_T_SIZE); }
  }

  public static final class Result extends Structure {
    public Pointer signed_data;
    public SizeT signed_data_len;
    public Pointer manifest_id;
    public Pointer manifest_json;
    public Pointer error_message;
    public int error_code;
    public Pointer output_path;

    protected List<String> getFieldOrder() {
      return Arrays.asList("signed_data", "signed_data_len",
        "manifest_id", "manifest_json", "error_message",
        "error_code", "output_path");
    }

    public Result(Pointer p) { super(p); read(); }
  }

  public interface 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);
    void c2pa_free_result(Pointer result);
  }

  public static void main(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) throw new IllegalStateException("MSL returned NULL");

    try {
      Result r = new Result(p);
      if (r.error_code != 0) {
        String m = r.error_message == null ? "Unknown error"
          : r.error_message.getString(0);
        throw new IllegalStateException(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

import com.sun.jna.*;
import java.util.*;

public final class MslAllFieldsExample {
  public static final class SizeT extends IntegerType {
    public SizeT() { super(Native.SIZE_T_SIZE); }
  }

  public static final class Result extends Structure {
    public Pointer signed_data;
    public SizeT signed_data_len;
    public Pointer manifest_id;
    public Pointer manifest_json;
    public Pointer error_message;
    public int error_code;
    public Pointer output_path;

    protected List<String> getFieldOrder() {
      return Arrays.asList("signed_data", "signed_data_len",
        "manifest_id", "manifest_json", "error_message",
        "error_code", "output_path");
    }

    public Result(Pointer pointer) { super(pointer); read(); }
  }

  public interface 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);
    void c2pa_free_result(Pointer result);
  }

  public static void main(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) {
      throw new IllegalStateException("MSL returned NULL");
    }

    try {
      Result result = new Result(pointer);
      if (result.error_code != 0) {
        String message = result.error_message == null
          ? "Unknown error" : result.error_message.getString(0);
        throw new IllegalStateException(
          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);
    }
  }
}

10.3 Example C: In-memory standard signing

Use c2pa_sign_content when the application already holds the complete source asset in memory. The function returns the signed bytes through signed_data and their length through signed_data_len. Copy these bytes before calling c2pa_free_result.

import com.sun.jna.*;
import java.nio.file.*;
import java.util.*;

public final class MslInMemoryExample {
  public static final class SizeT extends IntegerType {
    public SizeT() { super(Native.SIZE_T_SIZE); }
    public SizeT(long value) { super(Native.SIZE_T_SIZE, value, true); }
  }

  public static final class Result extends Structure {
    public Pointer signed_data;
    public SizeT signed_data_len;
    public Pointer manifest_id;
    public Pointer manifest_json;
    public Pointer error_message;
    public int error_code;
    public Pointer output_path;

    protected List<String> getFieldOrder() {
      return Arrays.asList("signed_data", "signed_data_len",
        "manifest_id", "manifest_json", "error_message",
        "error_code", "output_path");
    }

    public Result(Pointer pointer) { super(pointer); read(); }
  }

  public interface Msl extends Library {
    Pointer c2pa_sign_content(Pointer data, SizeT dataLength,
      String filename, String roles, String tsa, String account,
      String user, String actions, String metadata, String trace);
    void c2pa_free_result(Pointer result);
  }

  public static void main(String[] args) throws Exception {
    Msl lib = Native.load(
      "../rust_binary/libc2pa_rust.dylib", Msl.class);
    byte[] source = Files.readAllBytes(Path.of("../input/sample.jpeg"));
    if (source.length == 0) {
      throw new IllegalArgumentException("Input file is empty");
    }

    Memory input = new Memory(source.length);
    input.write(0, source, 0, source.length);
    Pointer pointer = lib.c2pa_sign_content(
      input, new SizeT(source.length), "sample.jpeg", "creator",
      "http://timestamp.digicert.com", System.getenv("MSL_ACCOUNT_ID"),
      null, null, null, null);

    if (pointer == null) {
      throw new IllegalStateException("MSL returned NULL");
    }

    try {
      Result result = new Result(pointer);
      if (result.error_code != 0) {
        String message = result.error_message == null
          ? "Unknown error" : result.error_message.getString(0);
        throw new IllegalStateException(
          result.error_code + ": " + message);
      }

      long length = result.signed_data_len.longValue();
      if (length > Integer.MAX_VALUE) {
        throw new IllegalStateException(
          "Signed output is too large for a Java byte array");
      }
      byte[] signed = result.signed_data.getByteArray(0, (int) length);
      Files.write(Path.of("../output/java_in_memory_signed.jpeg"), signed);
      System.out.println(
        "Manifest ID: " + result.manifest_id.getString(0));
    } finally {
      lib.c2pa_free_result(pointer);
    }
  }
}

In this Java example, JNA Memory holds the input bytes in native memory for the duration of the signing call. getByteArray copies the signed bytes into a Java byte array before c2pa_free_result releases the native result.

CAWG identity signing

CAWG identity signing adds verifiable identity information using an S/MIME Baseline Requirements credential.

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.

Restart after changing credential environment variables: MSL reads credential environment variables once, when the application starts. It uses those values until the application process ends.

After changing these variables, restart the application with the new values.

ModeBehaviour
SigningImplementation::CawgIdentityIn-memory CAWG identity signing
SigningImplementation::CawgIdentityFromPathPath-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

RoleIdentity field
creatorcawg.creator
publishercawg.publisher
contributorcawg.contributor
editorcawg.editor
producercawg.producer
sponsorcawg.sponsor
translatorcawg.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());
    if let Some(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);
    if let Some(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.

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

ConditionResultHandling
Invalid API key loaded at application startup5020 and HTTP 401 wrong_tokenCorrect C2PA_API_KEY in the application’s startup environment, update any source setting used to populate it, and restart the application before retrying.
Signing still succeeds after replacing the API key with an invalid value while the application is runningMSL continues using the previously loaded key, which may still be validRestart the application with the replacement key configured. Changes to environment variables do not update MSL’s active configuration.
An environment or credential configuration change has no effectMSL continues using the values loaded at application bootUpdate the application’s startup environment and restart every affected application process.
Existing output5022, destination existsUse a new path or remove the old file
Invalid role4001Use roles allowed for the mode
Invalid JSON or metadata4000Correct JSON shape or conditional fields
Missing input or libraryLocal path or load errorValidate paths and architecture
Modified signed filedataHash mismatchTreat 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 \
  "<absolute-path-reported-by-otool>" \
  "@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.

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

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

ComponentVersion or platformIntegration notes
macOS libraryUniversal arm64 and x86_64Use the .dylib package
RustRust 1.89.0; wrapper 1.1.1Use the Rust wrapper crate
PythonPython 3.9.6 or laterUse the standard ctypes module
CApple clang on arm64 or x86_64Include c2pa_msl.h and link the native library
Node.jsNode.js 26.0.0; Koffi 3.1.6Use Koffi to call the C-compatible interface
GoGo 1.26.7 on darwin/arm64Use cgo and the C header
JavaOpenJDK 21; JNA 5.17.0Use JNA to call the C-compatible interface
c2patool0.27.10Use 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 <stddef.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

/*
 * Result returned by c2pa_sign_content and c2pa_sign_content_from_path.
 * Read the fields before calling c2pa_free_result; do not free individual
 * fields with free().
 */
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;

/*
 * Result returned by c2pa_prepare_cawg_signing and c2pa_prepare_cawg_signing_from_path.
 * On success, contains the preparation token and hash to sign. On error, error_code
 * is non-zero and error_message is non-NULL. Free with c2pa_free_prepare_result.
 */
typedef struct {
  char *preparation_token;
  char *hash_to_sign_b64;
  char *sig_structure_b64;
  char *signer_payload_cbor_b64;
  char *assertion_json;
  int64_t cawg_algorithm;
  char *cawg_metadata_hash_b64;
  char *cawg_training_mining_hash_b64;
  int32_t error_code;
  char *error_message;
} C2paPrepareResult;

/*
 * Signs an in-memory asset. Required strings: filename, roles_csv, account_id.
 * Optional strings may be NULL: tsa_url, user_id, additional_actions_json,
 * signing_metadata_json, trace_id.
 */
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
);

/*
 * Signs an asset from input_path and writes it to output_path. Required strings:
 * input_path, output_path, filename, roles_csv, account_id. Optional strings
 * may be NULL: tsa_url, user_id, additional_actions_json,
 * signing_metadata_json, 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
);

/*
 * Signs content with CAWG identity assertion (one-shot convenience).
 * Generates CAWG metadata automatically from roles.
 * CAWG roles: creator, contributor, editor, producer, publisher, sponsor, translator.
 * Optional strings may be NULL: tsa_url, user_id, additional_actions_json,
 * signing_metadata_json, trace_id.
 */
C2paSignedResult *c2pa_sign_content_with_cawg_identity(
  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
);

/*
 * Path-based CAWG identity signing. Reads from input_path, writes to output_path.
 * Optional strings may be NULL: tsa_url, user_id, additional_actions_json,
 * signing_metadata_json, trace_id.
 */
C2paSignedResult *c2pa_sign_content_with_cawg_identity_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
);

/*
 * Two-step CAWG signing (prepare phase). Returns hash to sign.
 * Pass the result's hash_to_sign_b64 to an external signer, then call
 * c2pa_complete_cawg_signing with the preparation_token and raw signature.
 * Required strings: filename, cawg_certificate_chain_pem, hard_binding_hash_b64,
 * roles_csv, account_id. Optional strings may be NULL: tsa_url, user_id,
 * additional_actions_json, signing_metadata_json, trace_id.
 */
C2paPrepareResult *c2pa_prepare_cawg_signing(
  const uint8_t *file_data,
  size_t file_data_len,
  const char *filename,
  const char *cawg_certificate_chain_pem,
  const char *hard_binding_hash_b64,
  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
);

/*
 * Two-step CAWG signing (complete phase). Returns signed asset.
 * Must be called with preparation_token and signature from c2pa_prepare_cawg_signing.
 * Required strings: preparation_token, cawg_raw_signature_b64.
 * Optional: trace_id.
 */
C2paSignedResult *c2pa_complete_cawg_signing(
  const char *preparation_token,
  const char *cawg_raw_signature_b64,
  const char *trace_id
);

/*
 * Path-based two-step CAWG signing (prepare phase).
 * Required strings: input_path, output_path, filename, cawg_certificate_chain_pem,
 * hard_binding_hash_b64, roles_csv, account_id. Optional strings may be NULL:
 * tsa_url, user_id, additional_actions_json, signing_metadata_json, trace_id.
 */
C2paPrepareResult *c2pa_prepare_cawg_signing_from_path(
  const char *input_path,
  const char *output_path,
  const char *filename,
  const char *cawg_certificate_chain_pem,
  const char *hard_binding_hash_b64,
  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
);

/*
 * Path-based two-step CAWG signing (complete phase).
 * Required strings: preparation_token, cawg_raw_signature_b64.
 * Optional: trace_id.
 */
C2paSignedResult *c2pa_complete_cawg_signing_from_path(
  const char *preparation_token,
  const char *cawg_raw_signature_b64,
  const char *trace_id
);

/* Frees a signed result and every field owned by Rust. Accepts NULL. */
void c2pa_free_result(C2paSignedResult *result);

/* Frees a prepare result and every field owned by Rust. Accepts NULL. */
void c2pa_free_prepare_result(C2paPrepareResult *result);

/* Frees a string allocated by Rust. Accepts NULL. */
void c2pa_free_string(char *str);

#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.
  2. S/MIME BR certificate setup: Get an S/MIME BR certificate.
  3. Digital Source Type vocabulary: IPTC NewsCodes.