Skip to main content

Creator Assertions (CAWG) on Mobile

This tutorial is a high-level guide with sample code for implementing the Creator Assertions Working Group (CAWG) specifications using the C2PA SDK for Android and the C2PA SDK for Swift.

A plain C2PA manifest answers "what device or app produced this file, and what happened to it?". Creator Assertions answer a different question: "who is the person or organization behind it, and what do they permit?". It achieves this with a second signing identity. The C2PA claim is signed by the capturing app or device; the CAWG identity assertion is signed by a credential belonging to the creator. Both signatures end up in the same manifest, and a validating verifier can check them independently.

This guide covers three CAWG features and how they fit together:

  • Identity (cawg.identity): Who the identity of the signer is, and which assertions they vouch for
  • Metadata (cawg.metadata): Creator name, copyright, and other standard metadata
  • Training and data mining (cawg.training-mining): Whether the asset may be used for AI training or data mining

The latest specifications are always on the CAWG specs page.

In addition, the focus of this tutorial is on-device CAWG signing using a local private key and X.509 certificates, and NOT remote singing on a server or using Identity Claims Aggregation through W3C Verifiable Credentials. We expect to provide more guidance and support on the second approach in the near future.

Before You Start

SDK version. CAWG support requires at least c2pa-android 0.0.10 or c2pa-swift 0.0.13. Earlier releases can support the use of CAWG metadata or training-mining assertions, but do not include Signer.withCawgIdentity for CAWG Identity assertion signing.

Reference implementation. Every snippet below is taken from working code in the Proofmode for Android or Proofmode for iOS codebase, or in other mobile sample code available through this site.

# gradle/libs.versions.toml
[versions]
c2pa = "0.0.10"

[libraries]
c2pa = { module = "org.contentauth:c2pa", version.ref = "c2pa" }

What you already need working. This guide assumes you can already produce a signed C2PA manifest with the C2PA Android SDK (a Signer, a manifest definition, and a successful Builder.sign() call) or similar with C2PA Swift. CAWG is layered on top of that, not a replacement for it.


Step 1: Create or import a CAWG signing identity

The identity assertion is signed with a credential that represents a creator of some kind, possibly ranging from artist to producer, individual or organization. This credential is separate from the key that signs the C2PA claim. For CAWG signing, you need a private key and a matching X.509 certificate chain, both in "PEM" (short for Privacy Enhanced Mail, but no one calls it that) form.

CAWG support in this SDK has been tested with 256-bit elliptic curve keys on the NIST P-256 curve (secp256r1), signed using ECDSA with SHA-256: SigningAlgorithm.ES256 on Android and SigningAlgorithm.es256 on Apple platforms.

note

The identity signer in this sample is built with Signer.fromKeys() on Android and the Signer(certsPEM:privateKeyPEM:algorithm:) initializer on Apple platforms, both of which need the private key as a PEM string. It is not using hardware-backed keys, whose private material can never leave the secure element. It is possible to generate a hardware-backed key stored in a secure element, or even implement a remote signer with a key on a server, and use it with CAWG. We will leave that more advanced implementation as an exercise for the reader. The c2pa-android and c2pa-swift sample apps contain a variety of code samples on how to do this, including SecureEnclaveSigner and WebServiceSigner in the Swift SDK.

Generating a new identity

Generating an identity means creating the key pair, writing the private key PEM to <alias>.key, generating a PKCS#10 certificate signing request into <alias>.csr, and storing a certificate chain in <alias>.cert. On Android, createCawgKey() does all four steps; on Apple platforms, CertificateManager provides the certificate and CSR halves.

val keyAlias = "CAWG_SECURE_1"

val fileKey = File(context.filesDir, "$keyAlias.key")
val fileCert = File(context.filesDir, "$keyAlias.cert")

// Generate the key, CSR, and certificate on first run only.
if (!fileKey.exists()) {
Timber.d("Creating new CAWG identity key")
val country = Locale.getDefault().country
CAWGIdentityManager(context).createCawgKey(
keyAlias = keyAlias,
useHardware = false, // identity signing needs an exportable key
creatorName = creatorName,
country = country,
)
}

//the local files will now exist based on the keyAlias name above
val privateKey = fileKey.readText()
val certChain = fileCert.readText()

The certificate that is created with a freshly generated identity is self-signed. This is useful for development, but it will not chain to a public trust list, so verifiers will show the identity as untrusted. To get a credential that validates for other people, submit the generated CSR to a certificate authority that issues CAWG identity certificates, such as SSL.com or Trufo. Both offer individual- and organization-verified options. Any Certificate Authority on the Mozilla Trust List should be trusted for CAWG signatures.

Proofmode provides the CSR to the user through getCawgCSR() on Android, and through the string written by CertificateManager.createCSR(for:config:) on Apple platforms, so it can be copied out and sent to a CA. When the CA returns a signed certificate, or when a creator already holds a credential, swap it in by overwriting the two PEM files and discarding the now-stale CSR:

fun importCawgIdentity(privateKeyPem: String, certChainPem: String) {
File(context.filesDir, "$CAWG_KEY_ALIAS.key").writeText(privateKeyPem)
File(context.filesDir, "$CAWG_KEY_ALIAS.cert").writeText(certChainPem)
// The old CSR belongs to a key the user no longer holds.
File(context.filesDir, "$CAWG_KEY_ALIAS.csr").delete()
}

Building a Local Identity Signer

Once you hold both PEMs, the identity signer is an ordinary Signer. Note the argument order in both SDKs: certificate chain first, then private key.

val identitySigner = Signer.fromKeys(
certChain, // certsPEM
privateKey, // privateKeyPEM
SigningAlgorithm.ES256,
)

Hold on to this identitySigner instance, you will need it later.

warning

On Apple platforms, Signer.withCawgIdentity declares both of its signer parameters as sending, so the compiler rejects a signer that is still referenced elsewhere, such as one stored in a property. In Swift, construct both signers as fresh locals (or inline, as in Step 4) at the point where you combine them.

Implementing a Custom Signer

While it is not the focus of this guide, you can implement custom logic for CAWG signing, including remote signing, hardware-backed keys, or other advanced functionality. To do this, use the same callback signer that the primary C2PA signing mechanism uses. A callback signer takes the certificate chain up front and calls your closure with the bytes to sign, so the private key never has to exist as a PEM string.

withCawgIdentity() only asks each of its inputs for a signature, an algorithm, a certificate chain, and a reserve size, so a callback signer works in either role. The example below passes one as the identity signer while the claim signer stays as it is, but you can equally pass a callback signer as the claim signer, or use one in both roles.

// The certificate chain is supplied up front; the private key stays wherever it
// lives, such as StrongBox, an HSM, or a signing server.
val identitySigner = Signer.withCallback(
algorithm = SigningAlgorithm.ES256,
certificateChainPEM = identityCertsPem,
tsaURL = null,
) { data ->
// Return the signature over `data`.
myCustomCAWGSigningFunction(data)
}

// From here it behaves exactly like the PEM-backed signer from earlier.
val combinedSigner = Signer.withCawgIdentity(
c2pa = c2paSigner,
identity = identitySigner,
referencedAssertions = listOf("c2pa.actions", "cawg.training-mining", "cawg.metadata"),
roles = listOf("cawg.creator"),
)

Two details are worth knowing before you write the callback:

  • Either signature encoding works. For the ECDSA algorithms, the SDK accepts both a DER-encoded signature and a raw P1363 r || s pair, detecting DER and converting it. This is why the output of SecKeyCreateSignature, which is DER, can be returned unchanged, as SecureEnclaveSigner in the Swift SDK does.
  • The callback is synchronous. It runs on whichever thread is inside builder.sign(), and that call blocks until your closure returns. If the callback reaches out to a signing server, keep the whole sign operation off the main thread and give the request a sensible timeout.

Step 2: Add the Metadata Assertion

The CAWG metadata assertion carries descriptive metadata about the asset. The specification deliberately places no restriction on which fields you may include, so it works by embedding existing metadata vocabularies. The example below uses Dublin Core (dc) and Exif.

You declare the vocabularies you use in an @context block, then add key/value pairs using those prefixes.

// The values the creator configured
val cawgCreator = "Nathan Freitas"
val cawgRights = "© 2026 Nathan Freitas. All Rights Reserved."

// Declare the metadata vocabularies being used
val cawgContext = hashMapOf(
"dc" to "http://purl.org/dc/elements/1.1/",
"exif" to "http://ns.adobe.com/exif/1.0/",
)

// The metadata itself, keyed by vocabulary prefix
val cawgInfo = HashMap<String, String>()
if (cawgCreator.isNotEmpty()) {
cawgInfo["dc:creator"] = "[$cawgCreator]"
cawgInfo["Exif.Image.Artist"] = cawgCreator
}
if (cawgRights.isNotEmpty()) {
cawgInfo["dc:rights"] = cawgRights
cawgInfo["Exif.Image.Copyright"] = cawgRights
}

// Turn it into an assertion and add it to the manifest's assertion list
listAssertions.add(createCAWGMetadataAssertion(context, cawgContext, cawgInfo))

createCAWGMetadataAssertion() is a thin helper that wraps the two maps into an AssertionDefinition:

private fun createCAWGMetadataAssertion(
context: Context,
cawgContext: HashMap<String, String>,
cawgInfo: HashMap<String, String>,
): AssertionDefinition = AssertionDefinition.custom(
label = "cawg.metadata",
data = buildJsonObject {
put("@context", buildJsonObject {
for ((prefix, uri) in cawgContext) put(prefix, uri)
})
for ((key, value) in cawgInfo) put(key, value)
},
)

See the full helper in Proofmode.


Step 3: Add the Training and Data Mining Assertion

The CAWG training and data mining assertion records the creator's preferences about AI and data-mining use of the asset. It is an entries map, keyed by use category, where each value carries a use permission.

The specification defines four categories and three permission values:

CategoryCovers
cawg.data_miningData mining generally
cawg.ai_inferenceInference using an already-trained model
cawg.ai_trainingTraining a model of any kind
cawg.ai_generative_trainingTraining a generative AI model

Permitted values are "allowed", "notAllowed", and "constrained". Use constrained together with a constraint_info string that explains the terms; the specification advises that a verifier lacking further information should treat constrained as equivalent to notAllowed.

// Map each use category to a permission value.
val trainingMiningEntries = linkedMapOf(
"cawg.data_mining" to "allowed", // allow general data mining
"cawg.ai_generative_training" to "notAllowed", // no generative model training
"cawg.ai_training" to "notAllowed", // no model training at all
"cawg.ai_inference" to "allowed", // inference on trained models is fine
)

val trainingMining = AssertionDefinition.custom(
label = "cawg.training-mining",
data = buildJsonObject {
put("entries", buildJsonObject {
for ((category, use) in trainingMiningEntries) {
put(category, buildJsonObject { put("use", use) })
}
})
},
)

listAssertions.add(trainingMining)

Step 4: Combine the C2PA and identity signers

This is where CAWG joins the main signing flow. Signer.withCawgIdentity() takes your existing C2PA claim signer plus the identity signer from Step 1 and returns a single combined signer that emits the cawg.identity assertion alongside the C2PA claim signature.

// The role this actor played. See the named actor roles below.
val listRoles = listOf("cawg.creator")

val combinedSigner = Signer.withCawgIdentity(
c2pa = c2paSigner, // your existing claim signer (keystore, StrongBox, remote…)
identity = identitySigner, // from Step 1
referencedAssertions = listOf("c2pa.actions", "cawg.training-mining", "cawg.metadata"),
roles = listRoles,
)

referencedAssertions is the list of manifest assertion labels that the creator's signature vouches for. It is what turns the identity assertion from "this person exists" into "this person stands behind these specific claims". The identity specification requires the manifest's hard binding assertion to be covered as well; read back the signed manifest (Step 6) to confirm the final list is what you expect.

roles describes what the actor did. The identity specification defines seven named actor roles:

cawg.creator · cawg.contributor · cawg.editor · cawg.producer · cawg.publisher · cawg.sponsor · cawg.translator

For a photo captured in-app, cawg.creator is almost always the right choice. Custom values are permitted if they follow the namespace conventions.

Things to look out for
  • Never cache and reuse a signer across sign operations when CAWG is enabled. The second call throws c2pa signer is already closed on Android, and Signer was consumed by withCawgIdentity(...) on Apple platforms. Create a fresh signer for each file you sign.
  • The two signers must be distinct instances. Passing the same one twice throws C2PAError.Api on Android and C2PAError.api("claim and identity signers must be distinct instances") on Apple platforms.
  • Do not call withCawgIdentity() concurrently with close() on the same signer; it reads the input pointers without synchronization.

Step 5: Configure the builder and sign

With the assertions built and the signers combined, the remaining work is ordinary C2PA signing.

Settings: created vs. gathered assertions

C2PA distinguishes assertions the claim generator created itself from those it merely gathered. The SDK decides which bucket an assertion lands in by comparing its label against builder.created_assertion_labels. Builder.DEFAULT_CREATED_ASSERTION_LABELS on Android and ManifestValidator.defaultCreatedAssertionLabels on Apple platforms cover the common ones: c2pa.actions, c2pa.actions.v2, and the thumbnail and ingredient labels. Add your own labels to that list for anything your app generates directly.

val createdLabels = Builder.DEFAULT_CREATED_ASSERTION_LABELS + listOf(
"proofmode.metadata",
"c2pa.metadata",
)

val settingsJson = buildJsonObject {
put("version", 1)
put("builder", buildJsonObject {
put("created_assertion_labels", buildJsonArray {
for (label in createdLabels) add(label)
})
})
put("trust", buildJsonObject {
put("trust_config", trustConfig) // PEM/config loaded from app assets
})
}

trustConfig is a trust configuration the app ships as an asset on Android, or as a bundle resource on Apple platforms, and reads at startup; it tells the SDK which certificate authorities to accept when validating. You can also set custom trust anchors and allowed list by bundling the list of C2PA Trusted Certificate Authorities.

Build and sign

Now that the combined signer is ready, you can create a Builder from the C2PA context, and proceed with the reset of the C2PA signing as normal.

// Apply the settings, then build from the resulting context
val settings = C2PASettings.create().apply {
updateFromString(settingsJson.toString(), "json")
}
val c2paContext = C2PAContext.fromSettings(settings)
val builder = Builder.fromContext(c2paContext).withDefinition(manifestJSON)
settings.close()

// Record a CREATED action for a fresh camera capture
val softwareAgent = "cawgTest-1.0"
val action = Action(
PredefinedAction.CREATED,
DigitalSourceType.DIGITAL_CAPTURE,
softwareAgent,
null,
)
builder.addAction(action)
builder.setIntent(BuilderIntent.Create(DigitalSourceType.DIGITAL_CAPTURE))

// Sign from a source stream into a destination stream with the combined signer
builder.sign(
format = contentType,
source = sourceStream,
dest = destStream,
signer = combinedSigner,
)

On Android, remember to close your streams and the combined signer in a finally block. Swift releases both when the last reference goes away, so there is nothing to close by hand — see signStream() in Proofmode for the full pattern, including the guarded close of the already-consumed base signer.


Step 6: Verify the result

Always read back what you signed. A manifest can be produced successfully and still not contain the identity assertion you expected. A mismatched referencedAssertions label or an untrusted certificate will not necessarily fail the signing call.

// Load the trust configuration used for validation
C2PA.loadSettings(settingsJson.toString(), "json")

val manifestJSON = C2PA.readFile(filePath, null)

val validation = ManifestValidator.validateJson(manifestJSON, logWarnings = true)
if (validation.hasErrors()) {
Timber.d("C2PA validation errors: ${validation.errors.joinToString("; ")}")
}

In the returned JSON, confirm that:

  • the active manifest contains a cawg.identity assertion;
  • its referenced_assertions lists the labels you passed in Step 4;
  • cawg.metadata and cawg.training-mining are present with the values you set;
  • the signature info reports the certificate you expect — not a leftover self-signed development certificate.

For an independent check, inspect the signed file with c2patool or upload it to the Content Credentials verify site.


Troubleshooting

SymptomCause and fix
c2pa signer is already closed, or Signer was consumed by withCawgIdentity(...)A signer was reused after withCawgIdentity() consumed it. Create a fresh signer for every sign operation.
c2pa and identity signers must be distinct instancesThe same Signer was passed as both the claim signer and the identity signer.
PEM Base64 error: invalid Base64 encodingA PEM block whose Base64 body is not wrapped at 64 characters. Regenerate it with PemWriter, or re-wrap pasted input.
No cawg.identity in the manifestThe combined signer was built but the original claim signer was passed to builder.sign(). Pass the value returned by withCawgIdentity().
Identity shows as untrusted in verifiersThe identity certificate is still the self-signed development one. Submit the CSR to a CA and import the issued chain.
referencedAssertions cannot exceed 255 entriesBoth referencedAssertions and roles are capped at 255 entries.
sending 'signer' risks causing data racesA Swift 6 compile error: a signer passed to withCawgIdentity is still referenced elsewhere. Build it as a fresh local or pass it inline.

Reference

Specifications

SDK and sample code

Certificate authorities issuing CAWG identity certificates