Skip to content

Commit a0293fd

Browse files
committed
LMS: HSSPrivateKeyParameters.rangeTestKeys rewritten as a break-out loop over the levels with the exhaustion test on a named per-level limit, its unreachable all-levels-exhausted message made grammatical, and Hierarchy taking over getLMSParameters; the level test deliberately uses the whole tree (2^h) and not the component key's own maxQ, which testNarrowedComponentKeyIsNotReplaced now covers - a component key with a narrower usage limit is left to refuse for itself, where a maxQ test would replace the level with a fresh full tree, lifting a limit the caller set
1 parent 215a358 commit a0293fd

3 files changed

Lines changed: 104 additions & 30 deletions

File tree

‎core/src/main/java/org/bouncycastle/crypto/params/HSSPrivateKeyParameters.java‎

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ List<LMSSignature> getSigList()
8686
return sigList;
8787
}
8888

89+
LMSParameters[] getLMSParameters()
90+
{
91+
LMSParameters[] parms = new LMSParameters[keys.length];
92+
93+
for (int i = 0; i < keys.length; i++)
94+
{
95+
parms[i] = keys[i].getLMSParameters();
96+
}
97+
98+
return parms;
99+
}
100+
89101
LMSPrivateKeyParameters[] copyKeys()
90102
{
91103
return (LMSPrivateKeyParameters[])keys.clone();
@@ -439,19 +451,7 @@ public synchronized long getIndex()
439451

440452
public LMSParameters[] getLMSParameters()
441453
{
442-
Hierarchy hierarchy = this.hierarchy;
443-
int len = hierarchy.size();
444-
445-
LMSParameters[] parms = new LMSParameters[len];
446-
447-
for (int i = 0; i < len; i++)
448-
{
449-
LMSPrivateKeyParameters lmsPrivateKey = hierarchy.getKey(i);
450-
451-
parms[i] = lmsPrivateKey.getLMSParameters();
452-
}
453-
454-
return parms;
454+
return hierarchy.getLMSParameters();
455455
}
456456

457457
synchronized void incIndex()
@@ -709,30 +709,36 @@ private void rangeTestKeys()
709709
{
710710
if (index >= indexLimit)
711711
{
712-
throw new ExhaustedPrivateKeyException(
713-
"hss private key" +
714-
((isShard) ? " shard" : "") +
715-
" is exhausted");
712+
throw new ExhaustedPrivateKeyException("hss private key" + (isShard ? " shard" : "") +
713+
" is exhausted");
716714
}
717715

718-
719716
int L = l;
720717
int d = L;
721-
Hierarchy prv = hierarchy;
722-
// >= rather than ==: an index above 2^h steps straight over an equality test
723-
// (github #2414). Decode now rejects such a q, so this is belt and braces.
724-
while (prv.getKey(d - 1).getIndex() >= 1 << (prv.getKey(d - 1).getSigParameters().getH()))
718+
Hierarchy currentHierarchy = this.hierarchy;
719+
while (true)
725720
{
726-
d = d - 1;
727-
if (d == 0)
721+
LMSPrivateKeyParameters key = currentHierarchy.getKey(d - 1);
722+
723+
// The whole tree, not the key's own maxQ: a component key given a narrower usage limit is
724+
// left to refuse for itself once it reaches it. Judging it by maxQ would replace the level
725+
// with a fresh full tree, lifting a limit the caller set and spending a one-time key of the
726+
// level above to sign it (testNarrowedComponentKeyIsNotReplaced).
727+
int keyIndexLimit = 1 << key.getSigParameters().getH();
728+
729+
// < rather than !=: an index above 2^h steps straight over an equality test
730+
// (github #2414). Decode now rejects such a q, so this is belt and braces.
731+
if (key.getIndex() < keyIndexLimit)
728732
{
729-
throw new ExhaustedPrivateKeyException(
730-
"hss private key" +
731-
((isShard) ? " shard" : "") +
732-
" is exhausted the maximum limit for this HSS private key");
733+
break;
733734
}
734-
}
735735

736+
if (--d == 0)
737+
{
738+
throw new ExhaustedPrivateKeyException("hss private key" + (isShard ? " shard" : "") +
739+
" has no one-time keys left at any level");
740+
}
741+
}
736742

737743
if (d < L)
738744
{

‎core/src/main/java/org/bouncycastle/crypto/params/LMSKeyParameters.java‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import java.io.ByteArrayOutputStream;
44
import java.io.IOException;
55

6-
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
76
import org.bouncycastle.util.Encodable;
87
import org.bouncycastle.util.Pack;
98

‎core/src/test/java/org/bouncycastle/crypto/params/HSSTests.java‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,75 @@ private static void expectRewindRefused(int l, List<LMSPrivateKeyParameters> key
15211521
}
15221522
}
15231523

1524+
/**
1525+
* A component key given a usage limit narrower than its tree keeps it. rangeTestKeys judges a
1526+
* level exhausted by the whole tree (2^h), not by the key's own maxQ: once the bottom key has
1527+
* given its one signature, the range test still passes the level and the bottom key's own claim
1528+
* refuses, leaving the HSS index where it was. A maxQ test would instead replace the level with
1529+
* a fresh full tree - lifting a limit the caller set, and spending a root one-time key to sign
1530+
* it. The bc-csharp IndexAndComponentIndexClaimedTogether test carries the same case.
1531+
*/
1532+
public void testNarrowedComponentKeyIsNotReplaced()
1533+
throws Exception
1534+
{
1535+
LMSigParameters sigParams = LMSigParameters.lms_sha256_n32_h5;
1536+
LMOtsParameters otsParams = LMOtsParameters.sha256_n32_w8;
1537+
int twoToH = 1 << sigParams.getH();
1538+
byte[] msg = Hex.decode("48656c6c6f");
1539+
1540+
byte[] I = Hex.decode("000102030405060708090a0b0c0d0e0f");
1541+
byte[] seed = Hex.decode("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20");
1542+
1543+
LMSPrivateKeyParameters root = new LMSPrivateKeyParameters(sigParams, otsParams, 0, I, twoToH, seed);
1544+
byte[][] child = root.deriveChildKey();
1545+
// one one-time key allowed, in a tree of 2^h
1546+
LMSPrivateKeyParameters bottom = new LMSPrivateKeyParameters(sigParams, otsParams, 0, child[0], 1, child[1]);
1547+
1548+
// the root signs the bottom key's public key, which advances the root's q to 1 - the
1549+
// position resetKeyToIndex expects of an intermediate level, so the key is kept as built
1550+
LMSSigner rootSigner = new LMSSigner();
1551+
rootSigner.init(true, root);
1552+
LMSSignature chain = LMSSignature.getInstance(
1553+
rootSigner.generateSignature(bottom.getPublicKey().getEncoded()));
1554+
1555+
List<LMSPrivateKeyParameters> keys = new ArrayList<LMSPrivateKeyParameters>();
1556+
keys.add(root);
1557+
keys.add(bottom);
1558+
List<LMSSignature> sigs = new ArrayList<LMSSignature>();
1559+
sigs.add(chain);
1560+
1561+
HSSPrivateKeyParameters hss = new HSSPrivateKeyParameters(2, keys, sigs, 0, (long)twoToH * twoToH);
1562+
1563+
assertSame("the bottom key was regenerated, so its usage limit is gone", bottom, hss.getKeys().get(1));
1564+
1565+
// the one signature the bottom key can give
1566+
HSSSigner signer = new HSSSigner();
1567+
signer.init(true, hss);
1568+
byte[] first = signer.generateSignature(msg);
1569+
HSSSigner verifier = new HSSSigner();
1570+
verifier.init(false, hss.getPublicKey());
1571+
assertTrue(verifier.verifySignature(msg, first));
1572+
assertEquals(1, hss.getIndex());
1573+
1574+
// the next passes the range test but is refused by the bottom key's own claim
1575+
try
1576+
{
1577+
signer.generateSignature(msg);
1578+
fail("a narrowed bottom key was replaced rather than refused");
1579+
}
1580+
catch (ExhaustedPrivateKeyException e)
1581+
{
1582+
assertEquals("ots private key exhausted", e.getMessage());
1583+
}
1584+
assertEquals("a refused claim moved the HSS index", 1, hss.getIndex());
1585+
assertSame("the refused level was replaced", bottom, hss.getKeys().get(1));
1586+
assertEquals(1, bottom.getIndex());
1587+
1588+
// and the key still encodes to something its own decoder accepts
1589+
HSSPrivateKeyParameters decoded = HSSPrivateKeyParameters.getInstance(hss.getEncoded());
1590+
assertEquals(1, decoded.getIndex());
1591+
}
1592+
15241593
/**
15251594
* The HSS index and the bottom key's one-time index q are claimed together. They are two
15261595
* records of the same position - checkIndexAgainstKeys requires them to agree at decode - and

0 commit comments

Comments
 (0)