Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions src/main/java/com/thealgorithms/ciphers/LamportSignature.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package com.thealgorithms.ciphers;

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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>Reference: <a href="https://en.wikipedia.org/wiki/Lamport_signature">Wikipedia: Lamport signature</a>
*/
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 (!MessageDigest.isEqual(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 AssertionError("SHA-256 is required by the Java SE specification", e);
}
}

static byte[] sha256(byte[] message, int offset, int length) {
return sha256(Arrays.copyOfRange(message, offset, offset + length));
}
}
101 changes: 101 additions & 0 deletions src/test/java/com/thealgorithms/ciphers/LamportSignatureTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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 java.util.Arrays;
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]);
}
}
}

@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);
}
}
Loading