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.
- Android (Gradle)
- Apple (Swift)
# gradle/libs.versions.toml
[versions]
c2pa = "0.0.10"
[libraries]
c2pa = { module = "org.contentauth:c2pa", version.ref = "c2pa" }
// Package.swift
dependencies: [
.package(url: "https://github.com/contentauth/c2pa-swift.git", from: "0.0.13")
]
targets: [
.target(
name: "YourTarget",
dependencies: [.product(name: "C2PA", package: "c2pa-swift")]
)
]
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.
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.
- Android (Kotlin)
- Apple (Swift)
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()
import C2PA
import CryptoKit
import Foundation
let keyAlias = "CAWG_SECURE_1"
let keyTag = "org.contentauth.cawg.\(keyAlias)"
let dir = URL.documentsDirectory
let fileKey = dir.appending(path: "\(keyAlias).key")
let fileCert = dir.appending(path: "\(keyAlias).cert")
let fileCSR = dir.appending(path: "\(keyAlias).csr")
// Generate the key, CSR, and certificate on first run only.
if !FileManager.default.fileExists(atPath: fileKey.path) {
// Identity signing needs an exportable key, so generate an ordinary keychain
// key rather than a Secure Enclave one.
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: keyTag,
],
]
var error: Unmanaged<CFError>?
guard let secPrivateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error),
let secPublicKey = SecKeyCopyPublicKey(secPrivateKey)
else {
throw error!.takeRetainedValue() as Error
}
let config = CertificateManager.CertificateConfig(
commonName: creatorName,
organization: creatorOrganization,
organizationalUnit: "Content Credentials",
country: Locale.current.region?.identifier ?? "US",
state: creatorState,
locality: creatorLocality
)
// A self-signed chain to sign with today, and a CSR to send to a CA.
let certChainPEM = try CertificateManager.createSelfSignedCertificateChain(
for: secPublicKey,
config: config
)
let csrPEM = try CertificateManager.createCSR(for: secPublicKey, config: config)
// SecKeyCopyExternalRepresentation returns the X9.63 form of the EC private key,
// which CryptoKit re-exports as a PKCS#8 PEM block.
guard let x963 = SecKeyCopyExternalRepresentation(secPrivateKey, nil) as Data? else {
throw CertificateManager.CertificateError.invalidKeyData
}
let privateKeyPEM = try P256.Signing.PrivateKey(x963Representation: x963).pemRepresentation
try privateKeyPEM.write(to: fileKey, atomically: true, encoding: .utf8)
try certChainPEM.write(to: fileCert, atomically: true, encoding: .utf8)
try csrPEM.write(to: fileCSR, atomically: true, encoding: .utf8)
}
// The local files now exist, based on the keyAlias name above.
let privateKey = try String(contentsOf: fileKey, encoding: .utf8)
let certChain = try String(contentsOf: fileCert, encoding: .utf8)
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:
- Android (Kotlin)
- Apple (Swift)
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()
}
func importCawgIdentity(privateKeyPEM: String, certChainPEM: String) throws {
let dir = URL.documentsDirectory
try privateKeyPEM.write(
to: dir.appending(path: "\(cawgKeyAlias).key"), atomically: true, encoding: .utf8)
try certChainPEM.write(
to: dir.appending(path: "\(cawgKeyAlias).cert"), atomically: true, encoding: .utf8)
// The old CSR belongs to a key the user no longer holds.
try? FileManager.default.removeItem(at: dir.appending(path: "\(cawgKeyAlias).csr"))
}
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.
- Android (Kotlin)
- Apple (Swift)
val identitySigner = Signer.fromKeys(
certChain, // certsPEM
privateKey, // privateKeyPEM
SigningAlgorithm.ES256,
)
let identitySigner = try Signer(
certsPEM: certChain, // certificate chain
privateKeyPEM: privateKey, // private key
algorithm: .es256
)
Hold on to this identitySigner instance, you will need it later.
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.
- Android (Kotlin)
- Apple (Swift)
// 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"),
)
// The certificate chain is supplied up front; the private key stays wherever it
// lives, such as the Secure Enclave, an HSM, or a signing server.
let identitySigner = try Signer(
algorithm: .es256,
certificateChainPEM: identityCertsPEM
) { data in
// Return the signature over `data`.
try myCustomCAWGSigningFunction(data)
}
// From here it behaves exactly like the PEM-backed signer from earlier.
let combinedSigner = try Signer.withCawgIdentity(
c2paSigner,
identity: identitySigner,
referencedAssertions: ["c2pa.actions", "cawg.training-mining", "cawg.metadata"],
roles: ["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 || spair, detecting DER and converting it. This is why the output ofSecKeyCreateSignature, which is DER, can be returned unchanged, asSecureEnclaveSignerin 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.
- Android (Kotlin)
- Apple (Swift)
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.
You declare the vocabularies you use in an @context block, then add key/value pairs using those prefixes. AssertionDefinition.custom wraps any JSON-compatible dictionary in an AnyCodable payload.
// The values the creator configured
let cawgCreator = "Nathan Freitas"
let cawgRights = "© 2026 Nathan Freitas. All Rights Reserved."
// Declare the metadata vocabularies being used
var cawgData: [String: Any] = [
"@context": [
"dc": "http://purl.org/dc/elements/1.1/",
"exif": "http://ns.adobe.com/exif/1.0/",
]
]
// The metadata itself, keyed by vocabulary prefix
if !cawgCreator.isEmpty {
cawgData["dc:creator"] = [cawgCreator]
cawgData["Exif.Image.Artist"] = cawgCreator
}
if !cawgRights.isEmpty {
cawgData["dc:rights"] = cawgRights
cawgData["Exif.Image.Copyright"] = cawgRights
}
// Turn it into an assertion and add it to the manifest's assertion list
listAssertions.append(
AssertionDefinition.custom(label: "cawg.metadata", data: AnyCodable(cawgData))
)
See createC2PAMetadataAssertion() in C2PAHelper.swift for a version that also merges Exif and GPS fields read from the capture.
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:
| Category | Covers |
|---|---|
cawg.data_mining | Data mining generally |
cawg.ai_inference | Inference using an already-trained model |
cawg.ai_training | Training a model of any kind |
cawg.ai_generative_training | Training 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.
- Android (Kotlin)
- Apple (Swift)
// 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)
// Map each use category to a permission value.
let trainingMiningEntries: [String: String] = [
"cawg.data_mining": "allowed", // allow general data mining
"cawg.ai_generative_training": "notAllowed", // no generative model training
"cawg.ai_training": "notAllowed", // no model training at all
"cawg.ai_inference": "allowed", // inference on trained models is fine
]
// Each value is an object, so the map becomes [category: ["use": permission]]
let entries = trainingMiningEntries.mapValues { ["use": $0] }
let trainingMining = AssertionDefinition.custom(
label: "cawg.training-mining",
data: AnyCodable(["entries": entries])
)
listAssertions.append(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.
- Android (Kotlin)
- Apple (Swift)
// 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,
)
// The role this actor played. See the named actor roles below.
let listRoles = ["cawg.creator"]
// withCawgIdentity consumes both inputs, so construct each one inline.
let combinedSigner = try Signer.withCawgIdentity(
try Signer( // your existing claim signer
certsPEM: claimCertChain,
privateKeyPEM: claimPrivateKey,
algorithm: .es256
),
identity: try Signer( // the identity credential from Step 1
certsPEM: certChain,
privateKeyPEM: privateKey,
algorithm: .es256
),
referencedAssertions: ["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.
- Never cache and reuse a signer across sign operations when CAWG is enabled. The second call throws
c2pa signer is already closedon Android, andSigner 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.Apion Android andC2PAError.api("claim and identity signers must be distinct instances")on Apple platforms. - Do not call
withCawgIdentity()concurrently withclose()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.
- Android (Kotlin)
- Apple (Swift)
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
})
}
let createdLabels = ManifestValidator.defaultCreatedAssertionLabels + [
"proofmode.metadata",
"c2pa.metadata",
]
let settingsDefinition = C2PASettingsDefinition(
version: 1,
trust: TrustSettings(trustConfig: trustConfig), // PEM/config bundled with the app
builder: BuilderSettingsDefinition(createdAssertionLabels: createdLabels)
)
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.
- Android (Kotlin)
- Apple (Swift)
// 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,
)
// Apply the settings, then build from the resulting context
let settings = try C2PASettings(definition: settingsDefinition)
let c2paContext = try C2PAContext(settings: settings)
let builder = try Builder(context: c2paContext, manifestJSON: manifestJSON)
// Record a CREATED action for a fresh camera capture
let softwareAgent = "cawgTest-1.0"
try builder.addAction(
Action(
action: .created,
digitalSourceType: .digitalCapture,
softwareAgent: softwareAgent
)
)
try builder.setIntent(.create(.digitalCapture))
// Sign from a source stream into a destination stream with the combined signer
try builder.sign(
format: contentType,
source: sourceStream,
destination: 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.
- Android (Kotlin)
- Apple (Swift)
// 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("; ")}")
}
// Load the trust configuration used for validation
let settingsJSON = try settingsDefinition.toJSON()
try Signer.loadSettings(settingsJSON, format: .json)
let manifestJSON = try C2PA.readFile(at: fileURL)
// The manifest read back from the file reports its own validation state
if let data = manifestJSON.data(using: .utf8),
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let state = json["validation_state"] as? String {
print("C2PA validation state: \(state)")
}
In the returned JSON, confirm that:
- the active manifest contains a
cawg.identityassertion; - its
referenced_assertionslists the labels you passed in Step 4; cawg.metadataandcawg.training-miningare 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
| Symptom | Cause 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 instances | The same Signer was passed as both the claim signer and the identity signer. |
PEM Base64 error: invalid Base64 encoding | A 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 manifest | The 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 verifiers | The identity certificate is still the self-signed development one. Submit the CSR to a CA and import the issued chain. |
referencedAssertions cannot exceed 255 entries | Both referencedAssertions and roles are capped at 255 entries. |
sending 'signer' risks causing data races | A 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
- CAWG specifications index
- Identity assertion 1.1 — including named actor roles
- Metadata assertion 1.2 (draft)
- Training and data mining assertion 1.1
SDK and sample code
- c2pa-android SDK
- c2pa-android-example
- c2pa-swift SDK, including its example app
- Proofmode for Android C2PA implementation and Proofmode for iOS C2PA implementation: the reference implementations for this guide
Certificate authorities issuing CAWG identity certificates