From 44021120a9b655156af77202effbe6fa4b4d5ad3 Mon Sep 17 00:00:00 2001 From: Douglas Stebila Date: Mon, 2 Nov 2026 11:45:27 -0500 Subject: [PATCH 1/2] Make dispose idempotent and reject use after dispose dispose_sig() and dispose_KEM() freed the native liboqs object but left its pointer in the Java object. Calling dispose twice freed it twice, and calling any other method afterwards used freed memory. Clear the handle when freeing, skip the free if it is already clear, and throw a RuntimeException from methods that need the native object once it has been disposed. Signed-off-by: Douglas Stebila Co-Authored-By: Claude Opus 5.5 --- src/main/c/KeyEncapsulation.c | 3 ++ src/main/c/Signature.c | 3 ++ .../org/openquantumsafe/KeyEncapsulation.java | 14 +++++++ .../java/org/openquantumsafe/Signature.java | 15 ++++++++ .../java/org/openquantumsafe/KEMTest.java | 36 ++++++++++++++++++ .../java/org/openquantumsafe/SigTest.java | 37 +++++++++++++++++++ 6 files changed, 108 insertions(+) diff --git a/src/main/c/KeyEncapsulation.c b/src/main/c/KeyEncapsulation.c index 83833b8..189e1c7 100644 --- a/src/main/c/KeyEncapsulation.c +++ b/src/main/c/KeyEncapsulation.c @@ -27,7 +27,10 @@ JNIEXPORT void JNICALL Java_org_openquantumsafe_KeyEncapsulation_free_1KEM (JNIEnv *env, jobject obj) { OQS_KEM *kem = (OQS_KEM *) getHandle(env, obj, "native_kem_handle_"); + if (kem == NULL) return; OQS_KEM_free(kem); + // Clear the handle so that the freed object cannot be used again + setHandle(env, obj, NULL, "native_kem_handle_"); } /* diff --git a/src/main/c/Signature.c b/src/main/c/Signature.c index 00efd07..84ff3dd 100644 --- a/src/main/c/Signature.c +++ b/src/main/c/Signature.c @@ -27,7 +27,10 @@ JNIEXPORT void JNICALL Java_org_openquantumsafe_Signature_free_1sig (JNIEnv *env, jobject obj) { OQS_SIG *sig = (OQS_SIG *) getHandle(env, obj, "native_sig_handle_"); + if (sig == NULL) return; OQS_SIG_free(sig); + // Clear the handle so that the freed object cannot be used again + setHandle(env, obj, NULL, "native_sig_handle_"); } /* diff --git a/src/main/java/org/openquantumsafe/KeyEncapsulation.java b/src/main/java/org/openquantumsafe/KeyEncapsulation.java index 2a88327..858fc56 100644 --- a/src/main/java/org/openquantumsafe/KeyEncapsulation.java +++ b/src/main/java/org/openquantumsafe/KeyEncapsulation.java @@ -163,12 +163,23 @@ public void dispose_KEM() { free_KEM(); } + /** + * \brief Throw RuntimeException if dispose_KEM() has freed the native + * object. + */ + private void check_not_disposed() throws RuntimeException { + if (native_kem_handle_ == 0) { + throw new RuntimeException("KeyEncapsulation has been disposed"); + } + } + /** * \brief Invoke native generate_keypair method using the PK and SK lengths * from alg_details_. Check return value and if != 0 throw Exception. */ public byte[] generate_keypair() throws RuntimeException { + check_not_disposed(); int rv_ = generate_keypair(this.public_key_, this.secret_key_); if (rv_ != 0) throw new RuntimeException("Cannot generate keypair"); return this.public_key_; @@ -179,6 +190,7 @@ public byte[] generate_keypair() throws RuntimeException { * from alg_details_. Check return value and if != 0 throw Exception. */ public byte[] generate_keypair(byte[] seed) throws RuntimeException { + check_not_disposed(); if (seed.length != alg_details_.length_keypair_seed) { throw new RuntimeException("Incorrect seed length"); } @@ -216,6 +228,7 @@ public byte[] export_secret_key() { */ public Pair encap_secret(byte[] public_key) throws RuntimeException { + check_not_disposed(); if (public_key.length != alg_details_.length_public_key) { throw new RuntimeException("Incorrect public key length"); } @@ -236,6 +249,7 @@ public Pair encap_secret(byte[] public_key) * \return Shared secret */ public byte[] decap_secret(byte[] ciphertext) throws RuntimeException { + check_not_disposed(); if (ciphertext.length != alg_details_.length_ciphertext) { throw new RuntimeException("Incorrect ciphertext length"); } diff --git a/src/main/java/org/openquantumsafe/Signature.java b/src/main/java/org/openquantumsafe/Signature.java index 2055877..546d1aa 100644 --- a/src/main/java/org/openquantumsafe/Signature.java +++ b/src/main/java/org/openquantumsafe/Signature.java @@ -200,11 +200,22 @@ public void dispose_sig() { free_sig(); } + /** + * \brief Throw RuntimeException if dispose_sig() has freed the native + * object. + */ + private void check_not_disposed() throws RuntimeException { + if (native_sig_handle_ == 0) { + throw new RuntimeException("Signature has been disposed"); + } + } + /** * \brief Invoke native generate_keypair method using the PK and SK lengths * from alg_details_. Check return value and if != 0 throw RuntimeException. */ public byte[] generate_keypair() throws RuntimeException { + check_not_disposed(); int rv_ = generate_keypair(this.public_key_, this.secret_key_); if (rv_ != 0) throw new RuntimeException("Cannot generate keypair"); return this.public_key_; @@ -230,6 +241,7 @@ public byte[] export_secret_key() { * \return signature */ public byte[] sign(byte[] message) throws RuntimeException { + check_not_disposed(); if (this.secret_key_.length != alg_details_.length_secret_key) { throw new RuntimeException("Incorrect secret key length, " + "make sure you specify one in the " + @@ -256,6 +268,7 @@ public byte[] sign(byte[] message) throws RuntimeException { */ public boolean verify(byte[] message, byte[] signature, byte[] public_key) throws RuntimeException { + check_not_disposed(); if (public_key.length != alg_details_.length_public_key) { throw new RuntimeException("Incorrect public key length"); } @@ -273,6 +286,7 @@ public boolean verify(byte[] message, byte[] signature, byte[] public_key) * \return signature */ public byte[] sign(byte[] message, byte[] ctx) throws RuntimeException { + check_not_disposed(); if (this.secret_key_.length != alg_details_.length_secret_key) { throw new RuntimeException("Incorrect secret key length, " + "make sure you specify one in the " + @@ -303,6 +317,7 @@ public byte[] sign(byte[] message, byte[] ctx) throws RuntimeException { */ public boolean verify(byte[] message, byte[] signature, byte[] ctx, byte[] public_key) throws RuntimeException { + check_not_disposed(); if (public_key.length != alg_details_.length_public_key) { throw new RuntimeException("Incorrect public key length"); } diff --git a/src/test/java/org/openquantumsafe/KEMTest.java b/src/test/java/org/openquantumsafe/KEMTest.java index 1a5e682..07f181b 100644 --- a/src/test/java/org/openquantumsafe/KEMTest.java +++ b/src/test/java/org/openquantumsafe/KEMTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertArrayEquals; @@ -57,6 +58,41 @@ public void testAllKEMs(String kem_name) { System.out.println(sb.toString()); } + /** + * Test that disposing of a KEM twice is harmless + */ + @Test + public void testDisposeKEMTwice() { + KeyEncapsulation client = new KeyEncapsulation("ML-KEM-512"); + client.generate_keypair(); + + client.dispose_KEM(); + client.dispose_KEM(); + } + + /** + * Test that a KEM cannot be used after it has been disposed of + */ + @Test + public void testUseAfterDisposeKEMExpectedException() { + KeyEncapsulation client = new KeyEncapsulation("ML-KEM-512"); + byte[] client_public_key = client.generate_keypair(); + byte[] ciphertext = client.encap_secret(client_public_key).getLeft(); + byte[] seed = Rand.randombytes(client.get_keypair_seed_length()); + + client.dispose_KEM(); + + assertDisposed(() -> client.generate_keypair()); + assertDisposed(() -> client.generate_keypair(seed)); + assertDisposed(() -> client.encap_secret(client_public_key)); + assertDisposed(() -> client.decap_secret(ciphertext)); + } + + private static void assertDisposed(Executable executable) { + RuntimeException e = Assertions.assertThrows(RuntimeException.class, executable); + Assertions.assertEquals("KeyEncapsulation has been disposed", e.getMessage()); + } + /** * Test KEMs with derandomized keypair generation. */ diff --git a/src/test/java/org/openquantumsafe/SigTest.java b/src/test/java/org/openquantumsafe/SigTest.java index d1f283f..0302e29 100644 --- a/src/test/java/org/openquantumsafe/SigTest.java +++ b/src/test/java/org/openquantumsafe/SigTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -58,6 +59,42 @@ public void testAllSigs(String sig_name) { System.out.println(sb.toString()); } + /** + * Test that disposing of a Sig twice is harmless + */ + @Test + public void testDisposeSigTwice() { + Signature signer = new Signature("ML-DSA-44"); + signer.generate_keypair(); + + signer.dispose_sig(); + signer.dispose_sig(); + } + + /** + * Test that a Sig cannot be used after it has been disposed of + */ + @Test + public void testUseAfterDisposeSigExpectedException() { + byte[] context = "01234567890".getBytes(); + Signature signer = new Signature("ML-DSA-44"); + byte[] signer_public_key = signer.generate_keypair(); + byte[] signature = signer.sign(message); + + signer.dispose_sig(); + + assertDisposed(() -> signer.generate_keypair()); + assertDisposed(() -> signer.sign(message)); + assertDisposed(() -> signer.verify(message, signature, signer_public_key)); + assertDisposed(() -> signer.sign(message, context)); + assertDisposed(() -> signer.verify(message, signature, context, signer_public_key)); + } + + private static void assertDisposed(Executable executable) { + RuntimeException e = Assertions.assertThrows(RuntimeException.class, executable); + Assertions.assertEquals("Signature has been disposed", e.getMessage()); + } + /** * Test Sigs with context. */ From cc0e90a0967d322218647f6d809b18e7e2d492e3 Mon Sep 17 00:00:00 2001 From: Douglas Stebila Date: Tue, 3 Nov 2026 10:18:17 -0500 Subject: [PATCH 2/2] Throw IllegalStateException from every method after dispose Use IllegalStateException rather than RuntimeException for use after dispose. Also check in the methods that don't use the native object (export_public_key, export_secret_key, the print methods and get_keypair_seed_length), so that a disposed object can't be used at all. Before this, export_secret_key returned the zeroed secret key. Signed-off-by: Douglas Stebila Co-Authored-By: Claude Opus 5.5 --- .../java/org/openquantumsafe/KeyEncapsulation.java | 11 ++++++++--- src/main/java/org/openquantumsafe/Signature.java | 10 +++++++--- src/test/java/org/openquantumsafe/KEMTest.java | 7 ++++++- src/test/java/org/openquantumsafe/SigTest.java | 6 +++++- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/openquantumsafe/KeyEncapsulation.java b/src/main/java/org/openquantumsafe/KeyEncapsulation.java index 858fc56..9bbe09d 100644 --- a/src/main/java/org/openquantumsafe/KeyEncapsulation.java +++ b/src/main/java/org/openquantumsafe/KeyEncapsulation.java @@ -164,12 +164,12 @@ public void dispose_KEM() { } /** - * \brief Throw RuntimeException if dispose_KEM() has freed the native + * \brief Throw IllegalStateException if dispose_KEM() has freed the native * object. */ - private void check_not_disposed() throws RuntimeException { + private void check_not_disposed() throws IllegalStateException { if (native_kem_handle_ == 0) { - throw new RuntimeException("KeyEncapsulation has been disposed"); + throw new IllegalStateException("KeyEncapsulation has been disposed"); } } @@ -204,6 +204,7 @@ public byte[] generate_keypair(byte[] seed) throws RuntimeException { * \brief Return seed length */ public long get_keypair_seed_length() { + check_not_disposed(); return alg_details_.length_keypair_seed; } @@ -211,6 +212,7 @@ public long get_keypair_seed_length() { * \brief Return public key */ public byte[] export_public_key() { + check_not_disposed(); return this.public_key_; } @@ -218,6 +220,7 @@ public byte[] export_public_key() { * \brief Return secret key */ public byte[] export_secret_key() { + check_not_disposed(); return this.secret_key_; } @@ -269,6 +272,7 @@ public byte[] decap_secret(byte[] ciphertext) throws RuntimeException { * initialized, initialize it and fill it using native C code. */ public void print_KeyEncapsulation() { + check_not_disposed(); if (alg_details_ == null) { alg_details_ = get_KEM_details(); } @@ -281,6 +285,7 @@ public void print_KeyEncapsulation() { * is not initialized, initialize it and fill it using native C code. */ public void print_details() { + check_not_disposed(); if (alg_details_ == null) { alg_details_ = get_KEM_details(); } diff --git a/src/main/java/org/openquantumsafe/Signature.java b/src/main/java/org/openquantumsafe/Signature.java index 546d1aa..382e668 100644 --- a/src/main/java/org/openquantumsafe/Signature.java +++ b/src/main/java/org/openquantumsafe/Signature.java @@ -201,12 +201,12 @@ public void dispose_sig() { } /** - * \brief Throw RuntimeException if dispose_sig() has freed the native + * \brief Throw IllegalStateException if dispose_sig() has freed the native * object. */ - private void check_not_disposed() throws RuntimeException { + private void check_not_disposed() throws IllegalStateException { if (native_sig_handle_ == 0) { - throw new RuntimeException("Signature has been disposed"); + throw new IllegalStateException("Signature has been disposed"); } } @@ -225,6 +225,7 @@ public byte[] generate_keypair() throws RuntimeException { * \brief Return public key */ public byte[] export_public_key() { + check_not_disposed(); return this.public_key_; } @@ -232,6 +233,7 @@ public byte[] export_public_key() { * \brief Return secret key */ public byte[] export_secret_key() { + check_not_disposed(); return this.secret_key_; } @@ -335,6 +337,7 @@ public boolean verify(byte[] message, byte[] signature, byte[] ctx, byte[] publi * initialized, initialize it and fill it using native C code. */ public void print_signature() { + check_not_disposed(); if (alg_details_ == null) { alg_details_ = get_sig_details(); } @@ -346,6 +349,7 @@ public void print_signature() { * not initialized, initialize it and fill it using native C code. */ public void print_details() { + check_not_disposed(); if (alg_details_ == null) { alg_details_ = get_sig_details(); } diff --git a/src/test/java/org/openquantumsafe/KEMTest.java b/src/test/java/org/openquantumsafe/KEMTest.java index 07f181b..08edb3b 100644 --- a/src/test/java/org/openquantumsafe/KEMTest.java +++ b/src/test/java/org/openquantumsafe/KEMTest.java @@ -86,10 +86,15 @@ public void testUseAfterDisposeKEMExpectedException() { assertDisposed(() -> client.generate_keypair(seed)); assertDisposed(() -> client.encap_secret(client_public_key)); assertDisposed(() -> client.decap_secret(ciphertext)); + assertDisposed(() -> client.get_keypair_seed_length()); + assertDisposed(() -> client.export_public_key()); + assertDisposed(() -> client.export_secret_key()); + assertDisposed(() -> client.print_KeyEncapsulation()); + assertDisposed(() -> client.print_details()); } private static void assertDisposed(Executable executable) { - RuntimeException e = Assertions.assertThrows(RuntimeException.class, executable); + IllegalStateException e = Assertions.assertThrows(IllegalStateException.class, executable); Assertions.assertEquals("KeyEncapsulation has been disposed", e.getMessage()); } diff --git a/src/test/java/org/openquantumsafe/SigTest.java b/src/test/java/org/openquantumsafe/SigTest.java index 0302e29..9960c75 100644 --- a/src/test/java/org/openquantumsafe/SigTest.java +++ b/src/test/java/org/openquantumsafe/SigTest.java @@ -88,10 +88,14 @@ public void testUseAfterDisposeSigExpectedException() { assertDisposed(() -> signer.verify(message, signature, signer_public_key)); assertDisposed(() -> signer.sign(message, context)); assertDisposed(() -> signer.verify(message, signature, context, signer_public_key)); + assertDisposed(() -> signer.export_public_key()); + assertDisposed(() -> signer.export_secret_key()); + assertDisposed(() -> signer.print_signature()); + assertDisposed(() -> signer.print_details()); } private static void assertDisposed(Executable executable) { - RuntimeException e = Assertions.assertThrows(RuntimeException.class, executable); + IllegalStateException e = Assertions.assertThrows(IllegalStateException.class, executable); Assertions.assertEquals("Signature has been disposed", e.getMessage()); }