From fa23b1aef71be66620add9cee2adb732f7bb5991 Mon Sep 17 00:00:00 2001 From: Douglas Stebila Date: Mon, 2 Nov 2026 11:43:42 -0500 Subject: [PATCH] Handle null context and algorithm names without crashing the JVM Passing null as the context to Signature.sign(message, ctx) or Signature.verify(message, signature, ctx, public_key) crashed the JVM, because the JNI code passed the null array to GetByteArrayElements. A null algorithm name crashed the same way in GetStringUTFChars. Pass a null context to liboqs as NULL, and treat a null algorithm name as not enabled. Also initialize the signature length, so that a failed signing operation throws "Cannot sign message" instead of an exception built from an uninitialized length. Signed-off-by: Douglas Stebila Co-Authored-By: Claude Opus 5.5 --- src/main/c/KEMs.c | 1 + src/main/c/KeyEncapsulation.c | 2 + src/main/c/Rand.c | 1 + src/main/c/Signature.c | 16 +++-- src/main/c/Sigs.c | 1 + .../java/org/openquantumsafe/Signature.java | 4 +- .../java/org/openquantumsafe/KEMTest.java | 23 +++++++ .../java/org/openquantumsafe/RandTest.java | 17 +++++ .../java/org/openquantumsafe/SigTest.java | 62 +++++++++++++++++++ 9 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 src/test/java/org/openquantumsafe/RandTest.java diff --git a/src/main/c/KEMs.c b/src/main/c/KEMs.c index f3f95ad..736c32c 100644 --- a/src/main/c/KEMs.c +++ b/src/main/c/KEMs.c @@ -20,6 +20,7 @@ JNIEXPORT jint JNICALL Java_org_openquantumsafe_KEMs_max_1number_1KEMs JNIEXPORT jboolean JNICALL Java_org_openquantumsafe_KEMs_is_1KEM_1enabled (JNIEnv *env, jclass cls, jstring java_str) { + if (java_str == NULL) return JNI_FALSE; const char *str_native = (*env)->GetStringUTFChars(env, java_str, 0); int is_enabled = OQS_KEM_alg_is_enabled (str_native); (*env)->ReleaseStringUTFChars(env, java_str, str_native); diff --git a/src/main/c/KeyEncapsulation.c b/src/main/c/KeyEncapsulation.c index 83833b8..bad3ce1 100644 --- a/src/main/c/KeyEncapsulation.c +++ b/src/main/c/KeyEncapsulation.c @@ -10,6 +10,8 @@ JNIEXPORT void JNICALL Java_org_openquantumsafe_KeyEncapsulation_create_1KEM_1new (JNIEnv *env, jobject obj, jstring jstr) { + if (jstr == NULL) return; + // Create get a liboqs::OQS_KEM pointer const char *str_native = (*env)->GetStringUTFChars(env, jstr, 0); OQS_KEM *kem = OQS_KEM_new(str_native); diff --git a/src/main/c/Rand.c b/src/main/c/Rand.c index dc165ea..6beba48 100644 --- a/src/main/c/Rand.c +++ b/src/main/c/Rand.c @@ -36,6 +36,7 @@ JNIEXPORT jbyteArray JNICALL Java_org_openquantumsafe_Rand_randombytes JNIEXPORT jint JNICALL Java_org_openquantumsafe_Rand_randombytes_1switch_1algorithm_1native (JNIEnv *env, jclass cls, jstring jstr) { + if (jstr == NULL) return -1; const char *alg_name_native = (*env)->GetStringUTFChars(env, jstr, 0); int rv_ = OQS_randombytes_switch_algorithm(alg_name_native); (*env)->ReleaseStringUTFChars(env, jstr, alg_name_native); diff --git a/src/main/c/Signature.c b/src/main/c/Signature.c index 00efd07..bc81650 100644 --- a/src/main/c/Signature.c +++ b/src/main/c/Signature.c @@ -10,6 +10,8 @@ JNIEXPORT void JNICALL Java_org_openquantumsafe_Signature_create_1sig_1new (JNIEnv *env, jobject obj, jstring jstr) { + if (jstr == NULL) return; + // Create get a liboqs::OQS_SIG pointer const char *str_native = (*env)->GetStringUTFChars(env, jstr, 0); OQS_SIG *sig = OQS_SIG_new(str_native); @@ -121,7 +123,7 @@ JNIEXPORT jint JNICALL Java_org_openquantumsafe_Signature_sign jbyte *secret_key_native = (*env)->GetByteArrayElements(env, jsecret_key, 0); OQS_SIG *sig = (OQS_SIG *) getHandle(env, obj, "native_sig_handle_"); - size_t len_sig; + size_t len_sig = 0; OQS_STATUS rv_ = OQS_SIG_sign(sig, (uint8_t*)signature_native, &len_sig, (uint8_t*)message_native, message_len, @@ -188,11 +190,12 @@ JNIEXPORT jint JNICALL Java_org_openquantumsafe_Signature_sign_1with_1ctx_1str // Convert to jbyte arrays jbyte *signature_native = (*env)->GetByteArrayElements(env, jsignature, 0); jbyte *message_native = (*env)->GetByteArrayElements(env, jmessage, 0); - jbyte *ctx_native = (*env)->GetByteArrayElements(env, jctx, 0); + // A null ctx means no context string + jbyte *ctx_native = (jctx == NULL) ? NULL : (*env)->GetByteArrayElements(env, jctx, 0); jbyte *secret_key_native = (*env)->GetByteArrayElements(env, jsecret_key, 0); OQS_SIG *sig = (OQS_SIG *) getHandle(env, obj, "native_sig_handle_"); - size_t len_sig; + size_t len_sig = 0; OQS_STATUS rv_ = OQS_SIG_sign_with_ctx_str(sig, (uint8_t*)signature_native, &len_sig, (uint8_t*)message_native, message_len, (uint8_t*)ctx_native, ctx_len, @@ -214,7 +217,7 @@ JNIEXPORT jint JNICALL Java_org_openquantumsafe_Signature_sign_1with_1ctx_1str // Release C memory (*env)->ReleaseByteArrayElements(env, jsignature, signature_native, 0); (*env)->ReleaseByteArrayElements(env, jmessage, message_native, JNI_ABORT); - (*env)->ReleaseByteArrayElements(env, jctx, ctx_native, JNI_ABORT); + if (jctx != NULL) (*env)->ReleaseByteArrayElements(env, jctx, ctx_native, JNI_ABORT); (*env)->ReleaseByteArrayElements(env, jsecret_key, secret_key_native, JNI_ABORT); return (rv_ == OQS_SUCCESS) ? 0 : -1; @@ -233,7 +236,8 @@ JNIEXPORT jboolean JNICALL Java_org_openquantumsafe_Signature_verify_1with_1ctx_ // Convert to jbyte arrays jbyte *message_native = (*env)->GetByteArrayElements(env, jmessage, 0); jbyte *signature_native = (*env)->GetByteArrayElements(env, jsignature, 0); - jbyte *ctx_native = (*env)->GetByteArrayElements(env, jctx, 0); + // A null ctx means no context string + jbyte *ctx_native = (jctx == NULL) ? NULL : (*env)->GetByteArrayElements(env, jctx, 0); jbyte *public_key_native = (*env)->GetByteArrayElements(env, jpublic_key, 0); OQS_SIG *sig = (OQS_SIG *) getHandle(env, obj, "native_sig_handle_"); @@ -245,7 +249,7 @@ JNIEXPORT jboolean JNICALL Java_org_openquantumsafe_Signature_verify_1with_1ctx_ // Release C memory (*env)->ReleaseByteArrayElements(env, jsignature, signature_native, JNI_ABORT); (*env)->ReleaseByteArrayElements(env, jmessage, message_native, JNI_ABORT); - (*env)->ReleaseByteArrayElements(env, jctx, ctx_native, JNI_ABORT); + if (jctx != NULL) (*env)->ReleaseByteArrayElements(env, jctx, ctx_native, JNI_ABORT); (*env)->ReleaseByteArrayElements(env, jpublic_key, public_key_native, JNI_ABORT); return (rv_ == OQS_SUCCESS) ? JNI_TRUE : JNI_FALSE; diff --git a/src/main/c/Sigs.c b/src/main/c/Sigs.c index d6bdc8d..7f10297 100644 --- a/src/main/c/Sigs.c +++ b/src/main/c/Sigs.c @@ -20,6 +20,7 @@ JNIEXPORT jint JNICALL Java_org_openquantumsafe_Sigs_max_1number_1sigs JNIEXPORT jboolean JNICALL Java_org_openquantumsafe_Sigs_is_1sig_1enabled (JNIEnv *env, jclass cls, jstring jstr) { + if (jstr == NULL) return JNI_FALSE; const char *str_native = (*env)->GetStringUTFChars(env, jstr, 0); int is_enabled = OQS_SIG_alg_is_enabled (str_native); (*env)->ReleaseStringUTFChars(env, jstr, str_native); diff --git a/src/main/java/org/openquantumsafe/Signature.java b/src/main/java/org/openquantumsafe/Signature.java index 2055877..f3de6d8 100644 --- a/src/main/java/org/openquantumsafe/Signature.java +++ b/src/main/java/org/openquantumsafe/Signature.java @@ -269,7 +269,7 @@ public boolean verify(byte[] message, byte[] signature, byte[] public_key) /** * \brief Invoke native sign method * \param message - * \param ctx + * \param ctx Context string, or null for no context * \return signature */ public byte[] sign(byte[] message, byte[] ctx) throws RuntimeException { @@ -297,7 +297,7 @@ public byte[] sign(byte[] message, byte[] ctx) throws RuntimeException { * \brief Invoke native verify method * \param message * \param signature - * \param ctx + * \param ctx Context string, or null for no context * \param public_key * \return True if the signature is valid, false otherwise */ diff --git a/src/test/java/org/openquantumsafe/KEMTest.java b/src/test/java/org/openquantumsafe/KEMTest.java index 1a5e682..85ad5fd 100644 --- a/src/test/java/org/openquantumsafe/KEMTest.java +++ b/src/test/java/org/openquantumsafe/KEMTest.java @@ -102,6 +102,29 @@ public void testUnsupportedKEMExpectedException() { Assertions.assertThrows(MechanismNotSupportedError.class, () -> new KeyEncapsulation("MechanismNotSupported")); } + /** + * Test that a null algorithm name is rejected like an unsupported one + */ + @Test + public void testNullKEMNameExpectedException() { + Assertions.assertThrows(MechanismNotSupportedError.class, () -> new KeyEncapsulation(null)); + } + + /** + * Test that a null algorithm name leaves the existing mechanism in place + */ + @Test + public void testCreateKEMNewWithNullName() { + KeyEncapsulation client = new KeyEncapsulation("ML-KEM-512"); + KeyEncapsulation server = new KeyEncapsulation("ML-KEM-512"); + byte[] client_public_key = client.generate_keypair(); + + client.create_KEM_new(null); + + Pair server_pair = server.encap_secret(client_public_key); + assertArrayEquals(client.decap_secret(server_pair.getLeft()), server_pair.getRight()); + } + /** * Method to convert the list of KEMs to a stream for input to testAllKEMs */ diff --git a/src/test/java/org/openquantumsafe/RandTest.java b/src/test/java/org/openquantumsafe/RandTest.java new file mode 100644 index 0000000..a22deab --- /dev/null +++ b/src/test/java/org/openquantumsafe/RandTest.java @@ -0,0 +1,17 @@ +package org.openquantumsafe; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class RandTest { + + /** + * Test that a null algorithm name is rejected + */ + @Test + public void testNullRandAlgExpectedException() { + RuntimeException e = Assertions.assertThrows(RuntimeException.class, + () -> Rand.randombytes_switch_algorithm(null)); + Assertions.assertEquals("Cannot switch rand algorithm", e.getMessage()); + } +} diff --git a/src/test/java/org/openquantumsafe/SigTest.java b/src/test/java/org/openquantumsafe/SigTest.java index d1f283f..aebe3a7 100644 --- a/src/test/java/org/openquantumsafe/SigTest.java +++ b/src/test/java/org/openquantumsafe/SigTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.params.provider.MethodSource; // import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; @@ -88,6 +89,46 @@ public void testSigsWithContext(String sig_name) { System.out.println(sb.toString()); } + /** + * Test that a null context means no context + */ + @ParameterizedTest(name = "Testing {arguments}") + @MethodSource("getContextSupportedAlgsAsStream") + public void testSigsWithNullContext(String sig_name) { + byte[] context = "01234567890".getBytes(); + + // Create signer and verifier + Signature signer = new Signature(sig_name); + Signature verifier = new Signature(sig_name); + + // Generate signer key pair + byte[] signer_public_key = signer.generate_keypair(); + + // A null context signature verifies as a signature without context + byte[] signature = signer.sign(message, null); + assertTrue(verifier.verify(message, signature, signer_public_key), sig_name); + assertTrue(verifier.verify(message, signature, null, signer_public_key), sig_name); + + // A signature with a context does not verify under a null context + byte[] context_signature = signer.sign(message, context); + assertFalse(verifier.verify(message, context_signature, null, signer_public_key), sig_name); + } + + /** + * Test that a failed signing operation is reported as such. ML-DSA limits + * the context to 255 bytes. + */ + @ParameterizedTest(name = "Testing {arguments}") + @MethodSource("getContextSupportedAlgsAsStream") + public void testContextTooLongExpectedException(String sig_name) { + Signature signer = new Signature(sig_name); + signer.generate_keypair(); + + RuntimeException e = Assertions.assertThrows(RuntimeException.class, + () -> signer.sign(message, new byte[256])); + Assertions.assertEquals("Cannot sign message", e.getMessage()); + } + /** * Test the MechanismNotSupported Exception */ @@ -96,6 +137,27 @@ public void testUnsupportedSigExpectedException() { Assertions.assertThrows(MechanismNotSupportedError.class, () -> new Signature("MechanismNotSupported")); } + /** + * Test that a null algorithm name is rejected like an unsupported one + */ + @Test + public void testNullSigNameExpectedException() { + Assertions.assertThrows(MechanismNotSupportedError.class, () -> new Signature(null)); + } + + /** + * Test that a null algorithm name leaves the existing mechanism in place + */ + @Test + public void testCreateSigNewWithNullName() { + Signature signer = new Signature("ML-DSA-44"); + byte[] signer_public_key = signer.generate_keypair(); + + signer.create_sig_new(null); + + assertTrue(signer.verify(message, signer.sign(message), signer_public_key)); + } + /** * Method to convert the list of Sigs to a stream for input to testAllSigs */