Skip to content

Feature/hors signature - #7635

Merged
alxkm merged 4 commits into
TheAlgorithms:masterfrom
dilaraacetin:feature/hors-signature
Oct 5, 2026
Merged

alxkm merged 4 commits into
TheAlgorithms:masterfrom
dilaraacetin:feature/hors-signature

Conversation

@dilaraacetin

Copy link
Copy Markdown
Contributor

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 ciphers package. 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

  • Key generation: t = 2^τ random 32-byte secret values are generated; the public key is pk[i] = SHA-256(sk[i]).
  • Index derivation: SHA-256(message) is read as a big-endian bit string and split into k chunks of τ bits, each giving an index in [0, t).
  • Signing: the signature consists of the k secret values at the derived indices. Signing is deterministic.
  • Verification: the indices are recomputed, and each revealed value is hashed and compared with the public key entry at that index using MessageDigest.isEqual.

Changes

  • src/main/java/com/thealgorithms/ciphers/HorsSignature.java: implementation
  • src/test/java/com/thealgorithms/ciphers/HorsSignatureTest.java: JUnit 5 tests

No other files are modified.

Design notes

  • Default parameters t = 1024, k = 16 follow the paper's suggested parameter set. Valid ranges are t a power of two in [16, 65536] and k·log2(t) ≤ 256.
  • Index derivation is a single package-private method shared by sign and verify, so it can be tested with known vectors.
  • Duplicate indices are allowed, as in the original HORS. The Javadoc notes that FORS addresses this.
  • Few-time security: every signature reveals k secret values, so security decreases with each signature. This is documented in the Javadoc.
  • All returned arrays are deep-copied to keep internal state immutable.
  • Educational implementation only.

Tests

  • Valid signatures with default and parameterized (t, k) values.
  • Several messages signed and verified with the same key pair.
  • Deterministic signing.
  • Verification fails for a tampered message, a tampered signature element and a different public key.
  • Known-vector tests for index derivation, including chunks that cross byte boundaries.
  • Signature structure checks, input validation and immutability.

mvn clean verify passes locally, and all files are formatted with clang-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

@codecov-commenter

codecov-commenter commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.87500% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.94%. Comparing base (68dc5f5) to head (151f902).

Files with missing lines Patch % Lines
.../java/com/thealgorithms/ciphers/HorsSignature.java 96.87% 2 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@alxkm alxkm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 true

This 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:

  1. Add k as a parameter of verify. Validate it with validateK(k, tau), reject signatures whose length is not k, and use k instead of signature.length when 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;
}
  1. Add public int getK() so the parameter can be passed along with the public key.
  2. Update the Javadoc of verify. It currently says k is taken from the signature length; it should document the new k parameter instead.
  3. Update the existing tests to pass k (keyPair.getK(), or k in 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()));
}

@dilaraacetin

Copy link
Copy Markdown
Contributor Author

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 alxkm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Thank you for the contribution.

@alxkm
alxkm merged commit 26db7ba into TheAlgorithms:master Oct 5, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE REQUEST] Add HORS (Hash to Obtain Random Subset) few-time signature scheme

3 participants