Feature/hors signature - #7635
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #7635 +/- ##
============================================
+ Coverage 81.91% 81.94% +0.02%
- Complexity 8243 8272 +29
============================================
Files 839 840 +1
Lines 25801 25865 +64
Branches 5042 5052 +10
============================================
+ Hits 21135 21195 +60
- Misses 3879 3881 +2
- Partials 787 789 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
alxkm
left a comment
There was a problem hiding this comment.
Thanks, this is a clean implementation with good docs and tests. One thing needs fixing before merge.
verify takes k from the signature length, so whoever submits the signature chooses it. A valid signature truncated to its first element still verifies, because for k = 1 the single index comes from the same first τ bits of the digest:
byte[][] sig = keyPair.sign(MESSAGE);
HorsSignature.verify(MESSAGE, Arrays.copyOf(sig, 1), keyPair.getPublicKey()); // returns trueThis also allows forgery. After one signature with the default parameters, up to 16 of the 1024 secrets are known. An attacker needs about 64 tries to find a message whose first index is one of them, and can then submit a one-element signature. In the paper k is part of the public key, PK = (k, v₁, …, vₜ), so the verifier has to know it in advance.
Requested changes:
- Add
kas a parameter ofverify. Validate it withvalidateK(k, tau), reject signatures whose length is notk, and usekinstead ofsignature.lengthwhen deriving indices:
public static boolean verify(byte[] message, byte[][] signature, byte[][] publicKey, int k) {
if (message == null) {
throw new IllegalArgumentException("message must not be null");
}
validateValues(publicKey, "publicKey");
validateValues(signature, "signature");
int tau = tauOf(publicKey.length);
validateK(k, tau);
if (signature.length != k) {
throw new IllegalArgumentException("signature must contain exactly " + k + " values, got " + signature.length);
}
int[] indices = messageIndices(hash(message), k, tau);
for (int j = 0; j < k; j++) {
if (!MessageDigest.isEqual(hash(signature[j]), publicKey[indices[j]])) {
return false;
}
}
return true;
}- Add
public int getK()so the parameter can be passed along with the public key. - Update the Javadoc of
verify. It currently sayskis taken from the signature length; it should document the newkparameter instead. - Update the existing tests to pass
k(keyPair.getK(), orkin the parameterized test), and add a regression test:
@Test
void testTruncatedSignatureIsRejected() {
HorsSignature keyPair = new HorsSignature();
byte[][] truncated = Arrays.copyOf(keyPair.sign(MESSAGE), 1);
assertThrows(IllegalArgumentException.class, () -> HorsSignature.verify(MESSAGE, truncated, keyPair.getPublicKey(), keyPair.getK()));
}|
Thanks for catching this. verify now takes k as a parameter, rejects signatures whose length is not k, and getK() exposes it alongside the public key. I added the truncated-signature regression test plus checks for k mismatches. |
alxkm
left a comment
There was a problem hiding this comment.
Looks good. Thank you for the contribution.
Fixes #7634
Description
Adds an educational implementation of HORS (Hash to Obtain Random Subset), a hash-based few-time signature scheme (Reyzin & Reyzin, 2002), to the
cipherspackage. HORS is the predecessor of FORS, used inside SLH-DSA/SPHINCS+ (FIPS 205). Together with the existing Lamport, Winternitz OTS and Merkle Signature Scheme implementations, the repository now covers the core building blocks of SPHINCS+.How it works
t = 2^τrandom 32-byte secret values are generated; the public key ispk[i] = SHA-256(sk[i]).SHA-256(message)is read as a big-endian bit string and split intokchunks ofτbits, each giving an index in[0, t).ksecret values at the derived indices. Signing is deterministic.MessageDigest.isEqual.Changes
src/main/java/com/thealgorithms/ciphers/HorsSignature.java: implementationsrc/test/java/com/thealgorithms/ciphers/HorsSignatureTest.java: JUnit 5 testsNo other files are modified.
Design notes
t = 1024,k = 16follow the paper's suggested parameter set. Valid ranges areta power of two in[16, 65536]andk·log2(t) ≤ 256.signandverify, so it can be tested with known vectors.ksecret values, so security decreases with each signature. This is documented in the Javadoc.Tests
(t, k)values.mvn clean verifypasses locally, and all files are formatted withclang-format -i --style=file.References
Reyzin & Reyzin, "Better than BiBa: Short One-time Signatures with Fast Signing and Verifying", ACISP 2002: https://eprint.iacr.org/2002/014
https://en.wikipedia.org/wiki/Hash-based_cryptography
FIPS 205 (SLH-DSA): https://csrc.nist.gov/pubs/fips/205/final
I have read CONTRIBUTING.md.
This pull request is all my own work -- I have not plagiarized it.
All filenames are in PascalCase.
All functions and variable names follow Java naming conventions.
All new algorithms have a URL in their comments that points to Wikipedia or other similar explanations.
All new algorithms include a corresponding test class that validates their functionality.
All new code is formatted with
clang-format -i --style=file path/to/your/file.java