Skip to content

Commit 922c4d3

Browse files
authored
feat(ciphers): add Merkle signature scheme (#7629)
1 parent 2105b56 commit 922c4d3

2 files changed

Lines changed: 456 additions & 0 deletions

File tree

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
package com.thealgorithms.ciphers;
2+
3+
import java.security.MessageDigest;
4+
import java.security.NoSuchAlgorithmException;
5+
6+
/**
7+
* The Merkle signature scheme (MSS) turns a one-time signature scheme into a many-time scheme.
8+
*
9+
* <p>Key generation creates {@code 2^h} Winternitz one-time key pairs and hashes each WOTS public
10+
* key into a leaf of a binary hash tree of height {@code h}. The tree root is the single MSS public
11+
* key. Every signature uses the next unused leaf, so one key pair can sign {@code 2^h} messages.
12+
*
13+
* <p>A signature contains the leaf index, the WOTS signature, the WOTS public key of that leaf and
14+
* the authentication path: the {@code h} sibling hashes needed to recompute the root from the leaf.
15+
* The verifier checks the WOTS signature, hashes the WOTS public key into the leaf and walks up the
16+
* tree with the authentication path. Including the WOTS public key in the signature is an
17+
* educational simplification; XMSS instead recomputes it from the WOTS signature.
18+
*
19+
* <p>MSS is the core of XMSS (RFC 8391). This implementation is educational and must not be used in
20+
* production.
21+
*
22+
* <p>Reference: <a href="https://en.wikipedia.org/wiki/Merkle_signature_scheme">Wikipedia: Merkle signature scheme</a>
23+
*
24+
* @author dilaraacetin
25+
* @see WinternitzSignature
26+
* @see LamportSignature
27+
*/
28+
public final class MerkleSignatureScheme {
29+
30+
// SHA-256 output length in bytes
31+
private static final int HASH_BYTES = 32;
32+
private static final int MIN_HEIGHT = 2;
33+
private static final int MAX_HEIGHT = 10;
34+
private static final int DEFAULT_HEIGHT = 4;
35+
private static final int DEFAULT_W = 16;
36+
37+
private final int height;
38+
private final WinternitzSignature[] oneTimeKeys;
39+
// tree[level][node]: level 0 holds the leaves, level h holds the root
40+
private final byte[][][] tree;
41+
private int nextLeafIndex;
42+
43+
/**
44+
* Generates a new key pair with height {@code h = 4} (16 signatures) and {@code w = 16}.
45+
*/
46+
public MerkleSignatureScheme() {
47+
this(DEFAULT_HEIGHT, DEFAULT_W);
48+
}
49+
50+
/**
51+
* Generates a new key pair that can sign {@code 2^h} messages.
52+
*
53+
* @param h the tree height; must be between 2 and 10
54+
* @param w the Winternitz parameter of the one-time keys; must be 4, 16 or 256
55+
* @throws IllegalArgumentException if {@code h} or {@code w} is not supported
56+
*/
57+
public MerkleSignatureScheme(int h, int w) {
58+
if (h < MIN_HEIGHT || h > MAX_HEIGHT) {
59+
throw new IllegalArgumentException("h must be between " + MIN_HEIGHT + " and " + MAX_HEIGHT + ", got " + h);
60+
}
61+
this.height = h;
62+
int leafCount = 1 << h;
63+
oneTimeKeys = new WinternitzSignature[leafCount];
64+
tree = new byte[h + 1][][];
65+
tree[0] = new byte[leafCount][];
66+
for (int i = 0; i < leafCount; i++) {
67+
oneTimeKeys[i] = new WinternitzSignature(w);
68+
tree[0][i] = hashPublicKey(oneTimeKeys[i].getPublicKey());
69+
}
70+
for (int level = 1; level <= h; level++) {
71+
tree[level] = new byte[tree[level - 1].length / 2][];
72+
for (int i = 0; i < tree[level].length; i++) {
73+
tree[level][i] = hash(tree[level - 1][2 * i], tree[level - 1][2 * i + 1]);
74+
}
75+
}
76+
}
77+
78+
/**
79+
* Returns a copy of the public key, which is the root of the Merkle tree.
80+
*
81+
* @return the 32-byte tree root
82+
*/
83+
public byte[] getPublicKey() {
84+
return tree[height][0].clone();
85+
}
86+
87+
/**
88+
* Returns how many messages this key pair can still sign.
89+
*
90+
* @return the number of unused one-time keys
91+
*/
92+
public int remainingSignatures() {
93+
return oneTimeKeys.length - nextLeafIndex;
94+
}
95+
96+
/**
97+
* Signs a message with the next unused one-time key.
98+
*
99+
* @param message the message to sign
100+
* @return the Merkle signature
101+
* @throws IllegalArgumentException if the message is null
102+
* @throws IllegalStateException if all {@code 2^h} one-time keys have been used
103+
*/
104+
public MerkleSignature sign(byte[] message) {
105+
if (message == null) {
106+
throw new IllegalArgumentException("message must not be null");
107+
}
108+
if (nextLeafIndex >= oneTimeKeys.length) {
109+
throw new IllegalStateException("All " + oneTimeKeys.length + " signatures of this key pair have been used");
110+
}
111+
int leafIndex = nextLeafIndex++;
112+
WinternitzSignature oneTimeKey = oneTimeKeys[leafIndex];
113+
byte[][] wotsSignature = oneTimeKey.sign(message);
114+
115+
// collect the sibling of the current node on every level, from the leaf up to the root
116+
byte[][] authPath = new byte[height][];
117+
int index = leafIndex;
118+
for (int level = 0; level < height; level++) {
119+
authPath[level] = tree[level][index ^ 1];
120+
index >>= 1;
121+
}
122+
return new MerkleSignature(leafIndex, wotsSignature, oneTimeKey.getPublicKey(), authPath);
123+
}
124+
125+
/**
126+
* Verifies a Merkle signature against a public key (tree root). The tree height is taken from
127+
* the length of the authentication path.
128+
*
129+
* @param message the signed message
130+
* @param signature the signature to check
131+
* @param publicKey the tree root of the signer
132+
* @param w the Winternitz parameter used to create the key pair; must be 4, 16 or 256
133+
* @return true if the signature is valid for the message and public key, false otherwise
134+
* @throws IllegalArgumentException if an argument is null, {@code w} is unsupported, or the
135+
* public key or WOTS part of the signature is malformed
136+
*/
137+
public static boolean verify(byte[] message, MerkleSignature signature, byte[] publicKey, int w) {
138+
if (message == null) {
139+
throw new IllegalArgumentException("message must not be null");
140+
}
141+
if (signature == null) {
142+
throw new IllegalArgumentException("signature must not be null");
143+
}
144+
if (publicKey == null || publicKey.length != HASH_BYTES) {
145+
throw new IllegalArgumentException("publicKey must be exactly " + HASH_BYTES + " bytes long");
146+
}
147+
if (!WinternitzSignature.verify(message, signature.wotsSignature, signature.wotsPublicKey, w)) {
148+
return false;
149+
}
150+
151+
byte[] node = hashPublicKey(signature.wotsPublicKey);
152+
int index = signature.leafIndex;
153+
for (byte[] sibling : signature.authPath) {
154+
// an even index is a left child, an odd index a right child
155+
node = (index & 1) == 0 ? hash(node, sibling) : hash(sibling, node);
156+
index >>= 1;
157+
}
158+
return MessageDigest.isEqual(node, publicKey);
159+
}
160+
161+
/**
162+
* A Merkle signature: leaf index, WOTS signature, WOTS public key and authentication path.
163+
* All arrays are copied on construction and on access.
164+
*/
165+
public static final class MerkleSignature {
166+
private final int leafIndex;
167+
private final byte[][] wotsSignature;
168+
private final byte[][] wotsPublicKey;
169+
private final byte[][] authPath;
170+
171+
/**
172+
* Creates a signature from its parts.
173+
*
174+
* @param leafIndex the index of the leaf used for signing
175+
* @param wotsSignature the Winternitz signature of the message
176+
* @param wotsPublicKey the Winternitz public key of the leaf
177+
* @param authPath the sibling hashes from the leaf up to the root
178+
* @throws IllegalArgumentException if an argument is null, the authentication path length is
179+
* not between 2 and 10, an authentication path value is not 32 bytes long, or the leaf
180+
* index is outside the tree
181+
*/
182+
public MerkleSignature(int leafIndex, byte[][] wotsSignature, byte[][] wotsPublicKey, byte[][] authPath) {
183+
if (authPath == null || authPath.length < MIN_HEIGHT || authPath.length > MAX_HEIGHT) {
184+
throw new IllegalArgumentException("authPath must contain between " + MIN_HEIGHT + " and " + MAX_HEIGHT + " values");
185+
}
186+
for (byte[] sibling : authPath) {
187+
if (sibling == null || sibling.length != HASH_BYTES) {
188+
throw new IllegalArgumentException("authPath values must be exactly " + HASH_BYTES + " bytes long");
189+
}
190+
}
191+
if (leafIndex < 0 || leafIndex >= 1 << authPath.length) {
192+
throw new IllegalArgumentException("leafIndex must be between 0 and " + ((1 << authPath.length) - 1) + ", got " + leafIndex);
193+
}
194+
this.leafIndex = leafIndex;
195+
this.wotsSignature = deepCopy(wotsSignature, "wotsSignature");
196+
this.wotsPublicKey = deepCopy(wotsPublicKey, "wotsPublicKey");
197+
this.authPath = deepCopy(authPath, "authPath");
198+
}
199+
200+
/**
201+
* @return the index of the leaf used for signing
202+
*/
203+
public int getLeafIndex() {
204+
return leafIndex;
205+
}
206+
207+
/**
208+
* @return a copy of the Winternitz signature
209+
*/
210+
public byte[][] getWotsSignature() {
211+
return deepCopy(wotsSignature, "wotsSignature");
212+
}
213+
214+
/**
215+
* @return a copy of the Winternitz public key of the leaf
216+
*/
217+
public byte[][] getWotsPublicKey() {
218+
return deepCopy(wotsPublicKey, "wotsPublicKey");
219+
}
220+
221+
/**
222+
* @return a copy of the authentication path, ordered from the leaf level up to the root
223+
*/
224+
public byte[][] getAuthPath() {
225+
return deepCopy(authPath, "authPath");
226+
}
227+
}
228+
229+
/**
230+
* Hashes a WOTS public key into a Merkle leaf. Shared by key generation and verification.
231+
*/
232+
private static byte[] hashPublicKey(byte[][] wotsPublicKey) {
233+
return hash(wotsPublicKey);
234+
}
235+
236+
private static byte[] hash(byte[]... parts) {
237+
try {
238+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
239+
for (byte[] part : parts) {
240+
digest.update(part);
241+
}
242+
return digest.digest();
243+
} catch (NoSuchAlgorithmException e) {
244+
throw new AssertionError("SHA-256 is required by the Java SE specification", e);
245+
}
246+
}
247+
248+
private static byte[][] deepCopy(byte[][] values, String name) {
249+
if (values == null) {
250+
throw new IllegalArgumentException(name + " must not be null");
251+
}
252+
byte[][] copy = new byte[values.length][];
253+
for (int i = 0; i < values.length; i++) {
254+
if (values[i] == null) {
255+
throw new IllegalArgumentException(name + " values must not be null");
256+
}
257+
copy[i] = values[i].clone();
258+
}
259+
return copy;
260+
}
261+
}

0 commit comments

Comments
 (0)