Skip to content

Commit 26db7ba

Browse files
authored
Feature/hors signature (#7635)
* feat(ciphers): add HORS few-time signature scheme * test(ciphers): add tests for HORS signature * fix(ciphers): require k as a verify parameter in HORS
1 parent 68dc5f5 commit 26db7ba

2 files changed

Lines changed: 412 additions & 0 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package com.thealgorithms.ciphers;
2+
3+
import java.security.MessageDigest;
4+
import java.security.NoSuchAlgorithmException;
5+
import java.security.SecureRandom;
6+
7+
/**
8+
* HORS (Hash to Obtain Random Subset) is a hash-based few-time signature scheme by Reyzin and
9+
* Reyzin (2002).
10+
*
11+
* <p>The private key consists of {@code t = 2^τ} random 32-byte secrets and the public key contains
12+
* their SHA-256 hashes. To sign, the SHA-256 digest of the message is split into {@code k} chunks of
13+
* {@code τ} bits (most significant bit first); each chunk is an index into the key, and the secrets
14+
* at those {@code k} indices form the signature. The verifier derives the same indices and checks
15+
* that each revealed secret hashes to the matching public-key value. The same index may appear more
16+
* than once, as in the original scheme. Signing is deterministic.
17+
*
18+
* <p>HORS is a few-time scheme: each signature reveals {@code k} secrets, so after {@code r}
19+
* signatures an attacker knows at most {@code r·k} of them. If all indices of another message fall
20+
* into that set, its signature can be forged, so security decreases with every signature.
21+
*
22+
* <p>HORST compresses the large public key with a Merkle tree. FORS, used in SPHINCS+ and SLH-DSA
23+
* (FIPS 205), uses a forest of trees instead, which also removes the repeated-index weakness. This
24+
* implementation is educational and must not be used in production.
25+
*
26+
* <p>References: <a href="https://eprint.iacr.org/2002/014">Reyzin and Reyzin, Better than BiBa</a>,
27+
* <a href="https://en.wikipedia.org/wiki/Hash-based_cryptography">Wikipedia: Hash-based cryptography</a>
28+
*
29+
* @author dilaraacetin
30+
* @see LamportSignature
31+
* @see WinternitzSignature
32+
* @see MerkleSignatureScheme
33+
*/
34+
public final class HorsSignature {
35+
36+
// SHA-256 output length in bytes
37+
private static final int HASH_BYTES = 32;
38+
private static final int DIGEST_BITS = 256;
39+
private static final int MIN_T = 16;
40+
private static final int MAX_T = 65536;
41+
private static final int DEFAULT_T = 1024;
42+
private static final int DEFAULT_K = 16;
43+
44+
private final int k;
45+
private final int tau;
46+
private final byte[][] privateKey;
47+
private final byte[][] publicKey;
48+
49+
/**
50+
* Generates a new key pair with {@code t = 1024} and {@code k = 16}.
51+
*/
52+
public HorsSignature() {
53+
this(DEFAULT_T, DEFAULT_K);
54+
}
55+
56+
/**
57+
* Generates a new key pair.
58+
*
59+
* @param t the number of secrets; a power of two between 16 and 65536
60+
* @param k the number of secrets revealed per signature; {@code k·log2(t)} must not exceed 256
61+
* @throws IllegalArgumentException if {@code t} or {@code k} is not supported
62+
*/
63+
public HorsSignature(int t, int k) {
64+
this.tau = tauOf(t);
65+
validateK(k, tau);
66+
this.k = k;
67+
SecureRandom secureRandom = new SecureRandom();
68+
privateKey = new byte[t][HASH_BYTES];
69+
publicKey = new byte[t][];
70+
for (int i = 0; i < t; i++) {
71+
secureRandom.nextBytes(privateKey[i]);
72+
publicKey[i] = hash(privateKey[i]);
73+
}
74+
}
75+
76+
/**
77+
* Returns a copy of the public key.
78+
*
79+
* @return {@code t} hash values, each 32 bytes long
80+
*/
81+
public byte[][] getPublicKey() {
82+
return deepCopy(publicKey);
83+
}
84+
85+
/**
86+
* Returns the number of secrets revealed per signature. Like {@code t}, it is part of the public
87+
* key and is needed for verification.
88+
*
89+
* @return the parameter {@code k}
90+
*/
91+
public int getK() {
92+
return k;
93+
}
94+
95+
/**
96+
* Signs a message. Every signature reveals {@code k} secrets, so a key pair should only sign a
97+
* few messages.
98+
*
99+
* @param message the message to sign
100+
* @return the signature: {@code k} secrets of 32 bytes each
101+
* @throws IllegalArgumentException if the message is null
102+
*/
103+
public byte[][] sign(byte[] message) {
104+
if (message == null) {
105+
throw new IllegalArgumentException("message must not be null");
106+
}
107+
int[] indices = messageIndices(hash(message), k, tau);
108+
byte[][] signature = new byte[k][];
109+
for (int j = 0; j < k; j++) {
110+
signature[j] = privateKey[indices[j]].clone();
111+
}
112+
return signature;
113+
}
114+
115+
/**
116+
* Verifies a signature against a public key. {@code t} is taken from the public key length.
117+
* {@code k} is part of the public key and must be supplied by the verifier: taking it from the
118+
* signature would let an attacker submit a shorter signature that reveals fewer secrets.
119+
*
120+
* @param message the signed message
121+
* @param signature the signature to check
122+
* @param publicKey the public key of the signer
123+
* @param k the number of secrets per signature used by the signer (see {@link #getK()})
124+
* @return true if the signature is valid for the message and public key, false otherwise
125+
* @throws IllegalArgumentException if an argument is null, the public key length is not a
126+
* supported {@code t}, {@code k} is not supported for this {@code t}, a value is not 32
127+
* bytes long, or the signature does not contain exactly {@code k} values
128+
*/
129+
public static boolean verify(byte[] message, byte[][] signature, byte[][] publicKey, int k) {
130+
if (message == null) {
131+
throw new IllegalArgumentException("message must not be null");
132+
}
133+
validateValues(publicKey, "publicKey");
134+
validateValues(signature, "signature");
135+
int tau = tauOf(publicKey.length);
136+
validateK(k, tau);
137+
if (signature.length != k) {
138+
throw new IllegalArgumentException("signature must contain exactly " + k + " values, got " + signature.length);
139+
}
140+
141+
int[] indices = messageIndices(hash(message), k, tau);
142+
for (int j = 0; j < k; j++) {
143+
if (!MessageDigest.isEqual(hash(signature[j]), publicKey[indices[j]])) {
144+
return false;
145+
}
146+
}
147+
return true;
148+
}
149+
150+
/**
151+
* Splits the first {@code k·τ} bits of a digest into {@code k} unsigned {@code τ}-bit indices,
152+
* most significant bit first. Shared by sign and verify.
153+
*/
154+
static int[] messageIndices(byte[] digest, int k, int tau) {
155+
if ((long) k * tau > digest.length * 8L) {
156+
throw new IllegalArgumentException("digest is too short for " + k + " indices of " + tau + " bits");
157+
}
158+
int[] indices = new int[k];
159+
for (int j = 0; j < k; j++) {
160+
int index = 0;
161+
for (int i = 0; i < tau; i++) {
162+
int bit = j * tau + i;
163+
// bit 0 is the most significant bit of digest[0]; & 0xFF reads the byte as unsigned
164+
index = (index << 1) | (((digest[bit / 8] & 0xFF) >> (7 - bit % 8)) & 1);
165+
}
166+
indices[j] = index;
167+
}
168+
return indices;
169+
}
170+
171+
private static int tauOf(int t) {
172+
if (t < MIN_T || t > MAX_T || (t & (t - 1)) != 0) {
173+
throw new IllegalArgumentException("t must be a power of two between " + MIN_T + " and " + MAX_T + ", got " + t);
174+
}
175+
return Integer.numberOfTrailingZeros(t);
176+
}
177+
178+
private static void validateK(int k, int tau) {
179+
// k > DIGEST_BITS / tau is the same as k * tau > DIGEST_BITS, but cannot overflow
180+
if (k < 1 || k > DIGEST_BITS / tau) {
181+
throw new IllegalArgumentException("k must be at least 1 and k * log2(t) at most " + DIGEST_BITS + ", got k = " + k);
182+
}
183+
}
184+
185+
private static void validateValues(byte[][] values, String name) {
186+
if (values == null) {
187+
throw new IllegalArgumentException(name + " must not be null");
188+
}
189+
for (byte[] value : values) {
190+
if (value == null || value.length != HASH_BYTES) {
191+
throw new IllegalArgumentException(name + " values must be exactly " + HASH_BYTES + " bytes long");
192+
}
193+
}
194+
}
195+
196+
private static byte[] hash(byte[] data) {
197+
try {
198+
return MessageDigest.getInstance("SHA-256").digest(data);
199+
} catch (NoSuchAlgorithmException e) {
200+
throw new AssertionError("SHA-256 is required by the Java SE specification", e);
201+
}
202+
}
203+
204+
private static byte[][] deepCopy(byte[][] values) {
205+
byte[][] copy = new byte[values.length][];
206+
for (int i = 0; i < values.length; i++) {
207+
copy[i] = values[i].clone();
208+
}
209+
return copy;
210+
}
211+
}

0 commit comments

Comments
 (0)