diff --git a/library/src/androidTest/kotlin/org/contentauth/c2pa/AndroidSignerTests.kt b/library/src/androidTest/kotlin/org/contentauth/c2pa/AndroidSignerTests.kt index 9b28a0e..86db9bf 100644 --- a/library/src/androidTest/kotlin/org/contentauth/c2pa/AndroidSignerTests.kt +++ b/library/src/androidTest/kotlin/org/contentauth/c2pa/AndroidSignerTests.kt @@ -105,6 +105,30 @@ class AndroidSignerTests : SignerTests() { assertTrue(result.success, "StrongBox Availability test failed: ${result.message}") } + @Test + fun runTestSignWithContextFromSettings() = runBlocking { + val result = testSignWithContextFromSettings() + assertTrue(result.success, "Sign With Context (settings signer) test failed: ${result.message}") + } + + @Test + fun runTestSignUnsupportedFormat() = runBlocking { + val result = testSignUnsupportedFormat() + assertTrue(result.success, "Sign Unsupported Format test failed: ${result.message}") + } + + @Test + fun runTestSignWithContextFromJson() = runBlocking { + val result = testSignWithContextFromJson() + assertTrue(result.success, "Sign With Context (fromJson builder) test failed: ${result.message}") + } + + @Test + fun runTestSignWithContextWithoutSigner() = runBlocking { + val result = testSignWithContextWithoutSigner() + assertTrue(result.success, "Sign With Context (no signer) test failed: ${result.message}") + } + @Test fun runTestSignerFromSettingsToml() = runBlocking { val result = testSignerFromSettingsToml() diff --git a/library/src/main/jni/c2pa_jni.c b/library/src/main/jni/c2pa_jni.c index ce87047..3e48bd8 100644 --- a/library/src/main/jni/c2pa_jni.c +++ b/library/src/main/jni/c2pa_jni.c @@ -1034,34 +1034,9 @@ JNIEXPORT jint JNICALL Java_org_contentauth_c2pa_Builder_writeIngredientArchiveN return result; } -JNIEXPORT jobject JNICALL Java_org_contentauth_c2pa_Builder_signNative(JNIEnv *env, jobject obj, jlong builderPtr, jstring format, jlong sourceStreamPtr, jlong destStreamPtr, jlong signerPtr) { - if (builderPtr == 0 || format == NULL || sourceStreamPtr == 0 || destStreamPtr == 0 || signerPtr == 0) { - (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalArgumentException"), - "Builder, format, streams, and signer cannot be null"); - return NULL; - } - - struct C2paBuilder *builder = (struct C2paBuilder*)(uintptr_t)builderPtr; - const char *cformat = jstring_to_cstring(env, format); - if (cformat == NULL) { - return NULL; - } - - struct C2paStream *source = (struct C2paStream*)(uintptr_t)sourceStreamPtr; - struct C2paStream *dest = (struct C2paStream*)(uintptr_t)destStreamPtr; - struct C2paSigner *signer = (struct C2paSigner*)(uintptr_t)signerPtr; - - const unsigned char *manifestBytes = NULL; - int64_t size = c2pa_builder_sign(builder, cformat, source, dest, signer, &manifestBytes); - - release_cstring(env, format, cformat); - - if (size < 0) { - throw_c2pa_exception(env, "Failed to sign builder"); - return NULL; - } - - // Create result object +// Builds a Builder.SignResult from a sign call's outputs. Frees manifestBytes on every +// path; returns NULL (with the pending exception cleared) if construction fails. +static jobject build_sign_result(JNIEnv *env, int64_t size, const unsigned char *manifestBytes) { jclass resultClass = g_signResultClass; if (resultClass == NULL) { resultClass = (*env)->FindClass(env, "org/contentauth/c2pa/Builder$SignResult"); @@ -1073,7 +1048,7 @@ JNIEXPORT jobject JNICALL Java_org_contentauth_c2pa_Builder_signNative(JNIEnv *e return NULL; } } - + jmethodID constructor = (*env)->GetMethodID(env, resultClass, "", "(J[B)V"); if (constructor == NULL) { check_exception(env); @@ -1082,7 +1057,7 @@ JNIEXPORT jobject JNICALL Java_org_contentauth_c2pa_Builder_signNative(JNIEnv *e } return NULL; } - + jbyteArray jmanifestBytes = NULL; if (manifestBytes != NULL && size > 0) { jmanifestBytes = safe_new_byte_array(env, size); @@ -1090,24 +1065,85 @@ JNIEXPORT jobject JNICALL Java_org_contentauth_c2pa_Builder_signNative(JNIEnv *e c2pa_free(manifestBytes); return NULL; } - + (*env)->SetByteArrayRegion(env, jmanifestBytes, 0, size, (const jbyte*)manifestBytes); if (check_exception(env)) { c2pa_free(manifestBytes); return NULL; } - + } + if (manifestBytes != NULL) { c2pa_free(manifestBytes); } - + jobject result = (*env)->NewObject(env, resultClass, constructor, (jlong)size, jmanifestBytes); if (result == NULL) { check_exception(env); } - + return result; } +JNIEXPORT jobject JNICALL Java_org_contentauth_c2pa_Builder_signNative(JNIEnv *env, jobject obj, jlong builderPtr, jstring format, jlong sourceStreamPtr, jlong destStreamPtr, jlong signerPtr) { + if (builderPtr == 0 || format == NULL || sourceStreamPtr == 0 || destStreamPtr == 0 || signerPtr == 0) { + (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalArgumentException"), + "Builder, format, streams, and signer cannot be null"); + return NULL; + } + + struct C2paBuilder *builder = (struct C2paBuilder*)(uintptr_t)builderPtr; + const char *cformat = jstring_to_cstring(env, format); + if (cformat == NULL) { + return NULL; + } + + struct C2paStream *source = (struct C2paStream*)(uintptr_t)sourceStreamPtr; + struct C2paStream *dest = (struct C2paStream*)(uintptr_t)destStreamPtr; + struct C2paSigner *signer = (struct C2paSigner*)(uintptr_t)signerPtr; + + const unsigned char *manifestBytes = NULL; + int64_t size = c2pa_builder_sign(builder, cformat, source, dest, signer, &manifestBytes); + + release_cstring(env, format, cformat); + + // On failure, return NULL and let the Kotlin wrapper raise C2PAError from c2pa_error(). + if (size < 0) { + return NULL; + } + + return build_sign_result(env, size, manifestBytes); +} + +JNIEXPORT jobject JNICALL Java_org_contentauth_c2pa_Builder_signWithContextNative(JNIEnv *env, jobject obj, jlong builderPtr, jstring format, jlong sourceStreamPtr, jlong destStreamPtr) { + if (builderPtr == 0 || format == NULL || sourceStreamPtr == 0 || destStreamPtr == 0) { + (*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalArgumentException"), + "Builder, format, and streams cannot be null"); + return NULL; + } + + struct C2paBuilder *builder = (struct C2paBuilder*)(uintptr_t)builderPtr; + const char *cformat = jstring_to_cstring(env, format); + if (cformat == NULL) { + return NULL; + } + + struct C2paStream *source = (struct C2paStream*)(uintptr_t)sourceStreamPtr; + struct C2paStream *dest = (struct C2paStream*)(uintptr_t)destStreamPtr; + + // Signer comes from the builder's context (programmatic or from settings). + const unsigned char *manifestBytes = NULL; + int64_t size = c2pa_builder_sign_context(builder, cformat, source, dest, &manifestBytes); + + release_cstring(env, format, cformat); + + // On failure, return NULL and let the Kotlin wrapper raise C2PAError from c2pa_error(). + if (size < 0) { + return NULL; + } + + return build_sign_result(env, size, manifestBytes); +} + // New Builder methods JNIEXPORT jbyteArray JNICALL Java_org_contentauth_c2pa_Builder_dataHashedPlaceholderNative(JNIEnv *env, jobject obj, jlong builderPtr, jlong reservedSize, jstring format) { if (builderPtr == 0 || format == NULL || reservedSize <= 0) { diff --git a/library/src/main/kotlin/org/contentauth/c2pa/Builder.kt b/library/src/main/kotlin/org/contentauth/c2pa/Builder.kt index ca5aa21..5a5f35f 100644 --- a/library/src/main/kotlin/org/contentauth/c2pa/Builder.kt +++ b/library/src/main/kotlin/org/contentauth/c2pa/Builder.kt @@ -576,13 +576,30 @@ class Builder internal constructor(private var ptr: Long) : Closeable { * @throws C2PAError.Api if signing fails */ @Throws(C2PAError::class) - fun sign(format: String, source: Stream, dest: Stream, signer: Signer): SignResult { - val result = signNative(ptr, format, source.rawPtr, dest.rawPtr, signer.ptr) - if (result.size < 0) { - throw C2PAError.Api(C2PA.getError() ?: "Failed to sign") - } - return result - } + fun sign(format: String, source: Stream, dest: Stream, signer: Signer): SignResult = + signNative(ptr, format, source.rawPtr, dest.rawPtr, signer.ptr) + ?: throw C2PAError.Api(C2PA.getError() ?: "Failed to sign") + + /** + * Signs the manifest using the signer configured on the builder's [C2PAContext] — either set + * programmatically (`C2PAContextBuilder.setSigner`) or supplied via settings (`[signer.local]` + * / `[cawg_x509_signer]`) — and writes the signed asset to [dest]. + * + * Unlike [sign], no explicit [Signer] is passed; the builder's context must have a signer + * configured, or signing fails. Every builder has a context ([fromJson] creates one + * internally), so this also works for builders created with [fromJson] and a [C2PASettings] + * that carries a signer. Use this instead of the deprecated settings-based [Signer] factories. + * + * @param format The MIME type of the asset (e.g. "image/jpeg") + * @param source The input stream containing the original asset + * @param dest The output stream for the signed asset + * @return A [SignResult] containing the manifest size and optional manifest bytes + * @throws C2PAError.Api if signing fails (e.g. the context has no signer) + */ + @Throws(C2PAError::class) + fun signWithContext(format: String, source: Stream, dest: Stream): SignResult = + signWithContextNative(ptr, format, source.rawPtr, dest.rawPtr) + ?: throw C2PAError.Api(C2PA.getError() ?: "Failed to sign with context") /** * Creates a data-hashed placeholder for deferred signing workflows. @@ -815,7 +832,13 @@ class Builder internal constructor(private var ptr: Long) : Closeable { sourceHandle: Long, destHandle: Long, signerHandle: Long, - ): SignResult + ): SignResult? + private external fun signWithContextNative( + handle: Long, + format: String, + sourceHandle: Long, + destHandle: Long, + ): SignResult? private external fun dataHashedPlaceholderNative(handle: Long, reservedSize: Long, format: String): ByteArray? private external fun signDataHashedEmbeddableNative( handle: Long, diff --git a/library/src/main/kotlin/org/contentauth/c2pa/C2PA.kt b/library/src/main/kotlin/org/contentauth/c2pa/C2PA.kt index b2dce10..f1ac4fd 100644 --- a/library/src/main/kotlin/org/contentauth/c2pa/C2PA.kt +++ b/library/src/main/kotlin/org/contentauth/c2pa/C2PA.kt @@ -44,12 +44,20 @@ object C2PA { * Load settings from a string. * Returns the result code from the native call (0 for success). */ + @Deprecated( + "Global settings apply relies on the deprecated c2pa_load_settings. Configure settings " + + "in C2PASettings and build a C2PAContext instead.", + ) @JvmStatic fun loadSettingsResult(settings: String, format: String): Int = loadSettingsNative(settings, format) /** * Load settings from a string */ + @Deprecated( + "Global settings apply relies on the deprecated c2pa_load_settings. Configure settings " + + "in C2PASettings and build a C2PAContext instead.", + ) @Throws(C2PAError::class) fun loadSettings(settings: String, format: String) { executeC2PAOperation("Failed to load settings") { diff --git a/library/src/main/kotlin/org/contentauth/c2pa/Signer.kt b/library/src/main/kotlin/org/contentauth/c2pa/Signer.kt index b1953b1..9e76de8 100644 --- a/library/src/main/kotlin/org/contentauth/c2pa/Signer.kt +++ b/library/src/main/kotlin/org/contentauth/c2pa/Signer.kt @@ -116,6 +116,11 @@ class Signer internal constructor(internal var ptr: Long) : Closeable { * } * ``` */ + @Deprecated( + "Settings-based signers rely on deprecated core APIs (c2pa_load_settings / " + + "c2pa_signer_from_settings). Configure the signer in C2PASettings, build a " + + "C2PAContext, and sign via Builder.signWithContext().", + ) @JvmStatic @Throws(C2PAError::class) fun fromSettingsJson(settingsJson: String): Signer = fromSettings(settingsJson, "json") @@ -161,6 +166,11 @@ class Signer internal constructor(internal var ptr: Long) : Closeable { * referenced_assertions = ["cawg.training-mining"] * ``` */ + @Deprecated( + "Settings-based signers rely on deprecated core APIs (c2pa_load_settings / " + + "c2pa_signer_from_settings). Configure the signer in C2PASettings, build a " + + "C2PAContext, and sign via Builder.signWithContext().", + ) @JvmStatic @Throws(C2PAError::class) fun fromSettingsToml(settingsToml: String): Signer = fromSettings(settingsToml, "toml") @@ -173,6 +183,7 @@ class Signer internal constructor(internal var ptr: Long) : Closeable { * @return A new [Signer] instance configured according to the settings. * @throws C2PAError if the settings are invalid or the signer cannot be created. */ + @Suppress("DEPRECATION") @JvmStatic @Throws(C2PAError::class) private fun fromSettings(settings: String, format: String): Signer = @@ -195,6 +206,11 @@ class Signer internal constructor(internal var ptr: Long) : Closeable { * @param format The format of the settings string ("json" or "toml"). * @throws C2PAError if the settings are invalid. */ + @Deprecated( + "Global settings apply relies on the deprecated c2pa_load_settings. Configure settings " + + "in C2PASettings and build a C2PAContext instead.", + ) + @Suppress("DEPRECATION") @JvmStatic @Throws(C2PAError::class) fun loadSettings(settings: String, format: String) { diff --git a/test-app/app/src/main/kotlin/org/contentauth/c2pa/testapp/TestScreen.kt b/test-app/app/src/main/kotlin/org/contentauth/c2pa/testapp/TestScreen.kt index 56babd9..97a6c7c 100644 --- a/test-app/app/src/main/kotlin/org/contentauth/c2pa/testapp/TestScreen.kt +++ b/test-app/app/src/main/kotlin/org/contentauth/c2pa/testapp/TestScreen.kt @@ -225,6 +225,10 @@ private suspend fun runAllTests(context: Context): List = withContex results.add(signerTests.testStrongBoxSignerIntegration()) results.add(signerTests.testKeyStoreSignerKeyManagement()) results.add(signerTests.testStrongBoxAvailability()) + results.add(signerTests.testSignUnsupportedFormat()) + results.add(signerTests.testSignWithContextFromSettings()) + results.add(signerTests.testSignWithContextFromJson()) + results.add(signerTests.testSignWithContextWithoutSigner()) results.add(signerTests.testSignerFromSettingsToml()) results.add(signerTests.testSignerFromSettingsJson()) results.add(signerTests.testCawgCombinedPemSigner()) diff --git a/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/CoreTests.kt b/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/CoreTests.kt index 2a1b9fb..1bf41dc 100644 --- a/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/CoreTests.kt +++ b/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/CoreTests.kt @@ -332,6 +332,7 @@ abstract class CoreTests : TestBase() { } } + @Suppress("DEPRECATION") suspend fun testLoadSettings(): TestResult = withContext(Dispatchers.IO) { runTest("Load Settings") { val settingsJson = diff --git a/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/SignerTests.kt b/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/SignerTests.kt index 248ac02..eb22966 100644 --- a/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/SignerTests.kt +++ b/test-shared/src/main/kotlin/org/contentauth/c2pa/test/shared/SignerTests.kt @@ -24,7 +24,9 @@ import kotlinx.serialization.json.jsonPrimitive import org.contentauth.c2pa.Builder import org.contentauth.c2pa.ByteArrayStream import org.contentauth.c2pa.C2PA +import org.contentauth.c2pa.C2PAContext import org.contentauth.c2pa.C2PAError +import org.contentauth.c2pa.C2PASettings import org.contentauth.c2pa.CertificateManager import org.contentauth.c2pa.FileStream import org.contentauth.c2pa.KeyStoreSigner @@ -802,6 +804,185 @@ abstract class SignerTests : TestBase() { } } + suspend fun testSignWithContextFromSettings(): TestResult = withContext(Dispatchers.IO) { + runTest("Sign With Context (settings signer)") { + try { + val settingsToml = loadSharedResourceAsString("test_settings_with_cawg_signing.toml") + ?: throw IllegalArgumentException("Resource not found: test_settings_with_cawg_signing.toml") + + val sourceImageData = loadResourceAsBytes("pexels_asadphoto_457882") + + // Build a context from settings carrying [signer.local], create the builder from + // it, and let signWithContext draw the signer from the context. + val signedSize = C2PASettings.create().use { settings -> + settings.updateFromString(settingsToml, "toml") + C2PAContext.fromSettings(settings).use { context -> + Builder.fromContext(context).withDefinition(TEST_MANIFEST_JSON).use { builder -> + ByteArrayStream(sourceImageData).use { source -> + ByteArrayStream().use { dest -> + builder.signWithContext("image/jpeg", source, dest).size + } + } + } + } + } + + val success = signedSize > 0 + TestResult( + "Sign With Context (settings signer)", + success, + if (success) { + "Signed via the context's settings-configured signer" + } else { + "signWithContext produced no manifest" + }, + "Signed size: $signedSize", + ) + } catch (e: Exception) { + TestResult( + "Sign With Context (settings signer)", + false, + "signWithContext flow threw", + e.toString(), + ) + } + } + } + + suspend fun testSignUnsupportedFormat(): TestResult = withContext(Dispatchers.IO) { + runTest("Sign Unsupported Format") { + try { + val certPem = loadResourceAsString("es256_certs") + val keyPem = loadResourceAsString("es256_private") + val sourceImageData = loadResourceAsBytes("pexels_asadphoto_457882") + + // An unsupported format must surface as C2PAError, not a RuntimeException. + var thrown: C2PAError? = null + + Builder.fromJson(TEST_MANIFEST_JSON).use { builder -> + Signer.fromInfo(SignerInfo(SigningAlgorithm.ES256, certPem, keyPem)).use { signer -> + ByteArrayStream(sourceImageData).use { source -> + ByteArrayStream().use { dest -> + try { + builder.sign("application/x-unsupported", source, dest, signer) + } catch (e: C2PAError) { + thrown = e + } + } + } + } + } + + val success = thrown != null + TestResult( + "Sign Unsupported Format", + success, + if (success) { + "sign threw C2PAError for an unsupported format" + } else { + "sign did not throw for an unsupported format" + }, + "Thrown: $thrown", + ) + } catch (e: Exception) { + TestResult( + "Sign Unsupported Format", + false, + "Setup for the unsupported-format flow threw", + e.toString(), + ) + } + } + } + + suspend fun testSignWithContextFromJson(): TestResult = withContext(Dispatchers.IO) { + runTest("Sign With Context (fromJson builder)") { + try { + val settingsToml = loadSharedResourceAsString("test_settings_with_cawg_signing.toml") + ?: throw IllegalArgumentException("Resource not found: test_settings_with_cawg_signing.toml") + + val sourceImageData = loadResourceAsBytes("pexels_asadphoto_457882") + + // fromJson creates the builder's context internally; with settings carrying + // [signer.local], signWithContext draws the signer from that context. + val signedSize = C2PASettings.create().use { settings -> + settings.updateFromString(settingsToml, "toml") + Builder.fromJson(TEST_MANIFEST_JSON, settings).use { builder -> + ByteArrayStream(sourceImageData).use { source -> + ByteArrayStream().use { dest -> + builder.signWithContext("image/jpeg", source, dest).size + } + } + } + } + + val success = signedSize > 0 + TestResult( + "Sign With Context (fromJson builder)", + success, + if (success) { + "Signed via a fromJson builder's settings-configured signer" + } else { + "signWithContext produced no manifest from a fromJson builder" + }, + "Signed size: $signedSize", + ) + } catch (e: Exception) { + TestResult( + "Sign With Context (fromJson builder)", + false, + "fromJson signWithContext flow threw", + e.toString(), + ) + } + } + } + + suspend fun testSignWithContextWithoutSigner(): TestResult = withContext(Dispatchers.IO) { + runTest("Sign With Context (no signer)") { + try { + val sourceImageData = loadResourceAsBytes("pexels_asadphoto_457882") + + // A default context has no signer, so signWithContext must throw C2PAError. + var thrown: C2PAError? = null + + C2PAContext.create().use { context -> + Builder.fromContext(context).withDefinition(TEST_MANIFEST_JSON).use { builder -> + ByteArrayStream(sourceImageData).use { source -> + ByteArrayStream().use { dest -> + try { + builder.signWithContext("image/jpeg", source, dest) + } catch (e: C2PAError) { + thrown = e + } + } + } + } + } + + val success = thrown != null + TestResult( + "Sign With Context (no signer)", + success, + if (success) { + "signWithContext threw C2PAError for a signer-less context" + } else { + "signWithContext did not throw for a signer-less context" + }, + "Thrown: $thrown", + ) + } catch (e: Exception) { + TestResult( + "Sign With Context (no signer)", + false, + "Setup for the no-signer flow threw", + e.toString(), + ) + } + } + } + + @Suppress("DEPRECATION") suspend fun testSignerFromSettingsToml(): TestResult = withContext(Dispatchers.IO) { runTest("Signer From Settings (TOML)") { try { @@ -883,6 +1064,7 @@ abstract class SignerTests : TestBase() { } } + @Suppress("DEPRECATION") suspend fun testSignerFromSettingsJson(): TestResult = withContext(Dispatchers.IO) { runTest("Signer From Settings (JSON)") { try {