Skip to content

Commit 03dfb45

Browse files
committed
feat(ciphers): add Lamport one-time signature
1 parent dd8df80 commit 03dfb45

2 files changed

Lines changed: 236 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)