Skip to content

Commit 4b58d36

Browse files
authored
feat(ciphers): add Lamport one-time signature (#7617)
* feat(ciphers): add Lamport one-time signature * Fix for the check * fixing the bugs * comments resolved
1 parent 6ed9243 commit 4b58d36

2 files changed

Lines changed: 246 additions & 0 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.thealgorithms.ciphers;
2+
3+
import java.security.MessageDigest;
4+
import java.security.NoSuchAlgorithmException;
5+
import java.security.SecureRandom;
6+
import java.util.Arrays;
7+
8+
/**
9+
* Lamport one-time signatures (OTS) are a hash-based post-quantum signature scheme.
10+
*
11+
* <p>The scheme is based on a private key consisting of 256 pairs of random 32-byte secrets,
12+
* one pair per bit position of a SHA-256 digest. The public key contains the SHA-256 hashes of
13+
* those secrets.
14+
*
15+
* <p>To sign a message, the algorithm hashes the message and reveals the secret corresponding to
16+
* each bit value in the digest. Verification recomputes the digest and checks that each revealed
17+
* secret hashes to the expected public-key value.
18+
*
19+
* <p>This implementation is educational and intentionally keeps dependencies minimal. Each key pair
20+
* is strictly one-time: signing a second message with the same key is rejected.
21+
*
22+
* <p>Reference: <a href="https://en.wikipedia.org/wiki/Lamport_signature">Wikipedia: Lamport signature</a>
23+
*/
24+
public final class LamportSignature {
25+
26+
private static final int DIGEST_BITS = 256;
27+
private static final int SECRET_BYTES = 32;
28+
private static final int NUM_VALUES = 2;
29+
30+
private LamportSignature() {
31+
}
32+
33+
/**
34+
* A Lamport key pair containing both the private and public material.
35+
*/
36+
public static final class KeyPair {
37+
private final byte[][][] privateKey;
38+
private final byte[][][] publicKey;
39+
private boolean used;
40+
41+
private KeyPair(byte[][][] privateKey, byte[][][] publicKey) {
42+
this.privateKey = privateKey;
43+
this.publicKey = publicKey;
44+
}
45+
46+
/**
47+
* Signs a message using this key pair.
48+
*
49+
* @param message the message to sign
50+
* @return the signature bytes; exactly 256 secret values ordered by digest bit positions
51+
* @throws IllegalArgumentException if the message is null
52+
* @throws IllegalStateException if this key pair has already been used to sign a message
53+
*/
54+
public byte[] sign(byte[] message) {
55+
if (message == null) {
56+
throw new IllegalArgumentException("message must not be null");
57+
}
58+
if (used) {
59+
throw new IllegalStateException("This Lamport key pair can only sign one message");
60+
}
61+
used = true;
62+
63+
byte[] digest = sha256(message);
64+
byte[] signature = new byte[DIGEST_BITS * SECRET_BYTES];
65+
for (int bit = 0; bit < DIGEST_BITS; bit++) {
66+
int value = (digest[bit / 8] >> (7 - (bit % 8))) & 0x01;
67+
byte[] secret = privateKey[bit][value];
68+
System.arraycopy(secret, 0, signature, bit * SECRET_BYTES, SECRET_BYTES);
69+
}
70+
return signature;
71+
}
72+
73+
/**
74+
* Verifies a message signature against this public key.
75+
*
76+
* @param message the message to verify
77+
* @param signature the signature to verify
78+
* @return true if the signature is valid for the provided message and public key
79+
* @throws IllegalArgumentException if message or signature is null or malformed
80+
*/
81+
public boolean verify(byte[] message, byte[] signature) {
82+
if (message == null) {
83+
throw new IllegalArgumentException("message must not be null");
84+
}
85+
if (signature == null) {
86+
throw new IllegalArgumentException("signature must not be null");
87+
}
88+
if (signature.length != DIGEST_BITS * SECRET_BYTES) {
89+
throw new IllegalArgumentException("signature length must be exactly 8192 bytes");
90+
}
91+
92+
byte[] digest = sha256(message);
93+
for (int bit = 0; bit < DIGEST_BITS; bit++) {
94+
int value = (digest[bit / 8] >> (7 - (bit % 8))) & 0x01;
95+
byte[] revealedSecret = Arrays.copyOfRange(signature, bit * SECRET_BYTES, (bit + 1) * SECRET_BYTES);
96+
byte[] expectedHash = publicKey[bit][value];
97+
byte[] actualHash = sha256(revealedSecret);
98+
if (!MessageDigest.isEqual(actualHash, expectedHash)) {
99+
return false;
100+
}
101+
}
102+
return true;
103+
}
104+
105+
public byte[][][] getPrivateKey() {
106+
return privateKey;
107+
}
108+
109+
public byte[][][] getPublicKey() {
110+
return publicKey;
111+
}
112+
}
113+
114+
/**
115+
* Generates a new Lamport key pair.
116+
*
117+
* @return a fresh Lamport key pair
118+
*/
119+
public static KeyPair generateKeyPair() {
120+
SecureRandom secureRandom = new SecureRandom();
121+
byte[][][] privateKey = new byte[DIGEST_BITS][NUM_VALUES][SECRET_BYTES];
122+
byte[][][] publicKey = new byte[DIGEST_BITS][NUM_VALUES][SECRET_BYTES];
123+
124+
for (int bit = 0; bit < DIGEST_BITS; bit++) {
125+
for (int value = 0; value < NUM_VALUES; value++) {
126+
secureRandom.nextBytes(privateKey[bit][value]);
127+
publicKey[bit][value] = sha256(privateKey[bit][value]);
128+
}
129+
}
130+
return new KeyPair(privateKey, publicKey);
131+
}
132+
133+
static byte[] sha256(byte[] input) {
134+
try {
135+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
136+
return digest.digest(input);
137+
} catch (NoSuchAlgorithmException e) {
138+
throw new AssertionError("SHA-256 is required by the Java SE specification", e);
139+
}
140+
}
141+
142+
static byte[] sha256(byte[] message, int offset, int length) {
143+
return sha256(Arrays.copyOfRange(message, offset, offset + length));
144+
}
145+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.thealgorithms.ciphers;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import java.nio.charset.StandardCharsets;
9+
import java.util.Arrays;
10+
import org.junit.jupiter.api.Test;
11+
12+
class LamportSignatureTest {
13+
14+
@Test
15+
void testValidSignatureVerifies() {
16+
LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair();
17+
byte[] message = "hello lamport".getBytes(StandardCharsets.UTF_8);
18+
19+
byte[] signature = keyPair.sign(message);
20+
21+
assertTrue(keyPair.verify(message, signature));
22+
}
23+
24+
@Test
25+
void testTamperedMessageFailsVerification() {
26+
LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair();
27+
byte[] message = "hello lamport".getBytes(StandardCharsets.UTF_8);
28+
byte[] signature = keyPair.sign(message);
29+
30+
assertFalse(keyPair.verify("hello lamport!".getBytes(StandardCharsets.UTF_8), signature));
31+
}
32+
33+
@Test
34+
void testTamperedSignatureFailsVerification() {
35+
LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair();
36+
byte[] message = "hello lamport".getBytes(StandardCharsets.UTF_8);
37+
byte[] signature = keyPair.sign(message);
38+
39+
signature[0] = (byte) (signature[0] ^ 0xFF);
40+
41+
assertFalse(keyPair.verify(message, signature));
42+
}
43+
44+
@Test
45+
void testDifferentKeyPairDoesNotVerify() {
46+
LamportSignature.KeyPair keyPair1 = LamportSignature.generateKeyPair();
47+
LamportSignature.KeyPair keyPair2 = LamportSignature.generateKeyPair();
48+
byte[] message = "post-quantum".getBytes(StandardCharsets.UTF_8);
49+
50+
byte[] signature = keyPair1.sign(message);
51+
52+
assertFalse(keyPair2.verify(message, signature));
53+
}
54+
55+
@Test
56+
void testSecondSignatureThrowsException() {
57+
LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair();
58+
byte[] first = "first message".getBytes(StandardCharsets.UTF_8);
59+
byte[] second = "second message".getBytes(StandardCharsets.UTF_8);
60+
61+
keyPair.sign(first);
62+
63+
assertThrows(IllegalStateException.class, () -> keyPair.sign(second));
64+
}
65+
66+
@Test
67+
void testNullAndMalformedInput() {
68+
LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair();
69+
byte[] validMessage = "message".getBytes(StandardCharsets.UTF_8);
70+
byte[] validSignature = keyPair.sign(validMessage);
71+
72+
assertThrows(IllegalArgumentException.class, () -> keyPair.sign(null));
73+
assertThrows(IllegalArgumentException.class, () -> keyPair.verify(null, validSignature));
74+
assertThrows(IllegalArgumentException.class, () -> keyPair.verify(validMessage, null));
75+
assertThrows(IllegalArgumentException.class, () -> keyPair.verify(validMessage, new byte[1]));
76+
}
77+
78+
@Test
79+
void testPublicKeyMatchesPrivateKeyHash() {
80+
LamportSignature.KeyPair keyPair = LamportSignature.generateKeyPair();
81+
byte[][][] publicKey = keyPair.getPublicKey();
82+
byte[][][] privateKey = keyPair.getPrivateKey();
83+
84+
for (int bit = 0; bit < 256; bit++) {
85+
for (int value = 0; value < 2; value++) {
86+
byte[] hash = LamportSignature.sha256(privateKey[bit][value]);
87+
assertArrayEquals(hash, publicKey[bit][value]);
88+
}
89+
}
90+
}
91+
92+
@Test
93+
void testSha256SliceHelperMatchesRangeHash() {
94+
byte[] message = "hello lamport".getBytes(StandardCharsets.UTF_8);
95+
96+
byte[] expected = LamportSignature.sha256(Arrays.copyOfRange(message, 2, 12));
97+
byte[] actual = LamportSignature.sha256(message, 2, 10);
98+
99+
assertArrayEquals(expected, actual);
100+
}
101+
}

0 commit comments

Comments
 (0)