From 03dfb4550488de283d31521b45e9f63622bfb9fd Mon Sep 17 00:00:00 2001 From: nitin1053 Date: Fri, 25 Sep 2026 13:29:07 +0530 Subject: [PATCH 1/4] feat(ciphers): add Lamport one-time signature --- .../ciphers/LamportSignature.java | 146 ++++++++++++++++++ .../ciphers/LamportSignatureTest.java | 90 +++++++++++ 2 files changed, 236 insertions(+) create mode 100644 src/main/java/com/thealgorithms/ciphers/LamportSignature.java create mode 100644 src/test/java/com/thealgorithms/ciphers/LamportSignatureTest.java diff --git a/src/main/java/com/thealgorithms/ciphers/LamportSignature.java b/src/main/java/com/thealgorithms/ciphers/LamportSignature.java new file mode 100644 index 000000000000..0e15947a6670 --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/LamportSignature.java @@ -0,0 +1,146 @@ +package com.thealgorithms.ciphers; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; + +/** + * Lamport one-time signatures (OTS) are a hash-based post-quantum signature scheme. + * + *

The scheme is based on a private key consisting of 256 pairs of random 32-byte secrets, + * one pair per bit position of a SHA-256 digest. The public key contains the SHA-256 hashes of + * those secrets. + * + *

To sign a message, the algorithm hashes the message and reveals the secret corresponding to + * each bit value in the digest. Verification recomputes the digest and checks that each revealed + * secret hashes to the expected public-key value. + * + *

This implementation is educational and intentionally keeps dependencies minimal. Each key pair + * is strictly one-time: signing a second message with the same key is rejected. + * + *

Reference: Wikipedia: Lamport signature + */ +public final class LamportSignature { + + private static final int DIGEST_BITS = 256; + private static final int SECRET_BYTES = 32; + private static final int NUM_VALUES = 2; + + private LamportSignature() { + } + + /** + * A Lamport key pair containing both the private and public material. + */ + public static final class KeyPair { + private final byte[][][] privateKey; + private final byte[][][] publicKey; + private boolean used; + + private KeyPair(byte[][][] privateKey, byte[][][] publicKey) { + this.privateKey = privateKey; + this.publicKey = publicKey; + } + + /** + * Signs a message using this key pair. + * + * @param message the message to sign + * @return the signature bytes; exactly 256 secret values ordered by digest bit positions + * @throws IllegalArgumentException if the message is null + * @throws IllegalStateException if this key pair has already been used to sign a message + */ + public byte[] sign(byte[] message) { + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + if (used) { + throw new IllegalStateException("This Lamport key pair can only sign one message"); + } + used = true; + + byte[] digest = sha256(message); + byte[] signature = new byte[DIGEST_BITS * SECRET_BYTES]; + for (int bit = 0; bit < DIGEST_BITS; bit++) { + int value = (digest[bit / 8] >> (7 - (bit % 8))) & 0x01; + byte[] secret = privateKey[bit][value]; + System.arraycopy(secret, 0, signature, bit * SECRET_BYTES, SECRET_BYTES); + } + return signature; + } + + /** + * Verifies a message signature against this public key. + * + * @param message the message to verify + * @param signature the signature to verify + * @return true if the signature is valid for the provided message and public key + * @throws IllegalArgumentException if message or signature is null or malformed + */ + public boolean verify(byte[] message, byte[] signature) { + if (message == null) { + throw new IllegalArgumentException("message must not be null"); + } + if (signature == null) { + throw new IllegalArgumentException("signature must not be null"); + } + if (signature.length != DIGEST_BITS * SECRET_BYTES) { + throw new IllegalArgumentException("signature length must be exactly 8192 bytes"); + } + + byte[] digest = sha256(message); + for (int bit = 0; bit < DIGEST_BITS; bit++) { + int value = (digest[bit / 8] >> (7 - (bit % 8))) & 0x01; + byte[] revealedSecret = Arrays.copyOfRange(signature, bit * SECRET_BYTES, (bit + 1) * SECRET_BYTES); + byte[] expectedHash = publicKey[bit][value]; + byte[] actualHash = sha256(revealedSecret); + if (!Arrays.equals(actualHash, expectedHash)) { + return false; + } + } + return true; + } + + public byte[][][] getPrivateKey() { + return privateKey; + } + + public byte[][][] getPublicKey() { + return publicKey; + } + } + + /** + * Generates a new Lamport key pair. + * + * @return a fresh Lamport key pair + */ + public static KeyPair generateKeyPair() { + SecureRandom secureRandom = new SecureRandom(); + byte[][][] privateKey = new byte[DIGEST_BITS][NUM_VALUES][SECRET_BYTES]; + byte[][][] publicKey = new byte[DIGEST_BITS][NUM_VALUES][SECRET_BYTES]; + + for (int bit = 0; bit < DIGEST_BITS; bit++) { + for (int value = 0; value < NUM_VALUES; value++) { + secureRandom.nextBytes(privateKey[bit][value]); + publicKey[bit][value] = sha256(privateKey[bit][value]); + } + } + return new KeyPair(privateKey, publicKey); + } + + static byte[] sha256(byte[] input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return digest.digest(input); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is not available on this platform", e); + } + } + + static byte[] sha256(byte[] message, int offset, int length) { + return sha256(Arrays.copyOfRange(message, offset, offset + length)); + } +} diff --git a/src/test/java/com/thealgorithms/ciphers/LamportSignatureTest.java b/src/test/java/com/thealgorithms/ciphers/LamportSignatureTest.java new file mode 100644 index 000000000000..907c5468eceb --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/LamportSignatureTest.java @@ -0,0 +1,90 @@ +package com.thealgorithms.ciphers; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class LamportSignatureTest { + + @Test + void testValidSignatureVerifies() { + LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair(); + byte[] message = "hello lamport".getBytes(StandardCharsets.UTF_8); + + byte[] signature = keyPair.sign(message); + + assertTrue(keyPair.verify(message, signature)); + } + + @Test + void testTamperedMessageFailsVerification() { + LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair(); + byte[] message = "hello lamport".getBytes(StandardCharsets.UTF_8); + byte[] signature = keyPair.sign(message); + + assertFalse(keyPair.verify("hello lamport!".getBytes(StandardCharsets.UTF_8), signature)); + } + + @Test + void testTamperedSignatureFailsVerification() { + LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair(); + byte[] message = "hello lamport".getBytes(StandardCharsets.UTF_8); + byte[] signature = keyPair.sign(message); + + signature[0] = (byte) (signature[0] ^ 0xFF); + + assertFalse(keyPair.verify(message, signature)); + } + + @Test + void testDifferentKeyPairDoesNotVerify() { + LamportSignature.KeyPair keyPair1 = LamportSignature.generateKeyPair(); + LamportSignature.KeyPair keyPair2 = LamportSignature.generateKeyPair(); + byte[] message = "post-quantum".getBytes(StandardCharsets.UTF_8); + + byte[] signature = keyPair1.sign(message); + + assertFalse(keyPair2.verify(message, signature)); + } + + @Test + void testSecondSignatureThrowsException() { + LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair(); + byte[] first = "first message".getBytes(StandardCharsets.UTF_8); + byte[] second = "second message".getBytes(StandardCharsets.UTF_8); + + keyPair.sign(first); + + assertThrows(IllegalStateException.class, () -> keyPair.sign(second)); + } + + @Test + void testNullAndMalformedInput() { + LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair(); + byte[] validMessage = "message".getBytes(StandardCharsets.UTF_8); + byte[] validSignature = keyPair.sign(validMessage); + + assertThrows(IllegalArgumentException.class, () -> keyPair.sign(null)); + assertThrows(IllegalArgumentException.class, () -> keyPair.verify(null, validSignature)); + assertThrows(IllegalArgumentException.class, () -> keyPair.verify(validMessage, null)); + assertThrows(IllegalArgumentException.class, () -> keyPair.verify(validMessage, new byte[1])); + } + + @Test + void testPublicKeyMatchesPrivateKeyHash() { + LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair(); + byte[][][] publicKey = keyPair.getPublicKey(); + byte[][][] privateKey = keyPair.getPrivateKey(); + + for (int bit = 0; bit < 256; bit++) { + for (int value = 0; value < 2; value++) { + byte[] hash = LamportSignature.sha256(privateKey[bit][value]); + assertArrayEquals(hash, publicKey[bit][value]); + } + } + } +} From e335f952272634647742a3bda26b3ccf5402e51f Mon Sep 17 00:00:00 2001 From: nitin1053 Date: Fri, 25 Sep 2026 13:39:39 +0530 Subject: [PATCH 2/4] Fix for the check --- src/main/java/com/thealgorithms/ciphers/LamportSignature.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/thealgorithms/ciphers/LamportSignature.java b/src/main/java/com/thealgorithms/ciphers/LamportSignature.java index 0e15947a6670..6140569a1dec 100644 --- a/src/main/java/com/thealgorithms/ciphers/LamportSignature.java +++ b/src/main/java/com/thealgorithms/ciphers/LamportSignature.java @@ -1,6 +1,5 @@ package com.thealgorithms.ciphers; -import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; From 8f53ac6368fc171bd6a45f57adab9310b95ebfeb Mon Sep 17 00:00:00 2001 From: nitin1053 Date: Fri, 25 Sep 2026 13:53:45 +0530 Subject: [PATCH 3/4] fixing the bugs --- spotbugs-exclude.xml | 3 +++ .../com/thealgorithms/ciphers/LamportSignature.java | 2 +- .../thealgorithms/ciphers/LamportSignatureTest.java | 11 +++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 1c34e47fb651..4085d4c17b46 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -178,4 +178,7 @@ + + + diff --git a/src/main/java/com/thealgorithms/ciphers/LamportSignature.java b/src/main/java/com/thealgorithms/ciphers/LamportSignature.java index 6140569a1dec..def8336c1c27 100644 --- a/src/main/java/com/thealgorithms/ciphers/LamportSignature.java +++ b/src/main/java/com/thealgorithms/ciphers/LamportSignature.java @@ -95,7 +95,7 @@ public boolean verify(byte[] message, byte[] signature) { byte[] revealedSecret = Arrays.copyOfRange(signature, bit * SECRET_BYTES, (bit + 1) * SECRET_BYTES); byte[] expectedHash = publicKey[bit][value]; byte[] actualHash = sha256(revealedSecret); - if (!Arrays.equals(actualHash, expectedHash)) { + if (!MessageDigest.isEqual(actualHash, expectedHash)) { return false; } } diff --git a/src/test/java/com/thealgorithms/ciphers/LamportSignatureTest.java b/src/test/java/com/thealgorithms/ciphers/LamportSignatureTest.java index 907c5468eceb..99802f753a64 100644 --- a/src/test/java/com/thealgorithms/ciphers/LamportSignatureTest.java +++ b/src/test/java/com/thealgorithms/ciphers/LamportSignatureTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import org.junit.jupiter.api.Test; class LamportSignatureTest { @@ -87,4 +88,14 @@ void testPublicKeyMatchesPrivateKeyHash() { } } } + + @Test + void testSha256SliceHelperMatchesRangeHash() { + byte[] message = "hello lamport".getBytes(StandardCharsets.UTF_8); + + byte[] expected = LamportSignature.sha256(Arrays.copyOfRange(message, 2, 12)); + byte[] actual = LamportSignature.sha256(message, 2, 10); + + assertArrayEquals(expected, actual); + } } From 12be9a333e803f04a208dccf0302f2395ae7387e Mon Sep 17 00:00:00 2001 From: nitin1053 Date: Fri, 25 Sep 2026 15:26:13 +0530 Subject: [PATCH 4/4] comments resolved --- spotbugs-exclude.xml | 3 --- src/main/java/com/thealgorithms/ciphers/LamportSignature.java | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 4085d4c17b46..1c34e47fb651 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -178,7 +178,4 @@ - - - diff --git a/src/main/java/com/thealgorithms/ciphers/LamportSignature.java b/src/main/java/com/thealgorithms/ciphers/LamportSignature.java index def8336c1c27..c4a6459fd4c3 100644 --- a/src/main/java/com/thealgorithms/ciphers/LamportSignature.java +++ b/src/main/java/com/thealgorithms/ciphers/LamportSignature.java @@ -135,7 +135,7 @@ static byte[] sha256(byte[] input) { MessageDigest digest = MessageDigest.getInstance("SHA-256"); return digest.digest(input); } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("SHA-256 is not available on this platform", e); + throw new AssertionError("SHA-256 is required by the Java SE specification", e); } }