Skip to content

Commit 8a19e35

Browse files
committed
LMS: HSSPrivateKeyParameters refuses an index that would move a component key back within its own tree (checkNotRewound) - a rewound tree re-issues one-time keys whose signatures still verify, and the only route to it is a stale index given to the public constructor; with a test that the refusal bites and forward moves still reposition
1 parent b7f03ad commit 8a19e35

2 files changed

Lines changed: 113 additions & 5 deletions

File tree

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ private void resetKeyToIndex()
575575
Hierarchy oldHierarchy = hierarchy;
576576

577577
long[] qTreePath = new long[oldHierarchy.size()];
578-
long q = getIndex();
578+
long q = this.index;
579579

580580
for (int t = oldHierarchy.size() - 1; t >= 0; t--)
581581
{
@@ -606,6 +606,8 @@ private void resetKeyToIndex()
606606
// and cannot have changed - so this is the same tree at a different one-time key, and
607607
// the repositioned key keeps the tree the root has already built.
608608
//
609+
checkNotRewound(0, rootKey.getIndex() - (qTreePath.length > 1 ? 1 : 0), qTreePath[0]);
610+
609611
rootKey = rootKey.repositionTo((int)qTreePath[0]);
610612

611613
keys[0] = rootKey;
@@ -653,7 +655,9 @@ else if (!lmsQMatch)
653655
// rebuild it. The public key is unchanged either way, so the chaining signature
654656
// above it still stands and does not need making again.
655657
//
656-
keys[i] = keys[i].repositionTo((int)qTreePath[i]);
658+
checkNotRewound(i, oldKey.getIndex() - (i < qTreePath.length - 1 ? 1 : 0), qTreePath[i]);
659+
660+
keys[i] = oldKey.repositionTo((int)qTreePath[i]);
657661
changed = true;
658662
}
659663

@@ -668,6 +672,28 @@ else if (!lmsQMatch)
668672

669673
}
670674

675+
/**
676+
* A component key whose identifier and seed are unchanged is the same tree, and moving it back
677+
* within that tree would hand out one-time keys it has already used; a signature made with one
678+
* verifies, so nothing later would surface it. Every route here that the key controls moves
679+
* forward or stays put - extractKeyShard advances the index, and a decoded key's index already
680+
* agrees with its component keys - so a position behind the key can only be a stale index
681+
* supplied to the public constructor, and it is refused rather than acted on.
682+
*
683+
* @param level the level being repositioned, for the message.
684+
* @param currentQ the one-time key the level has advanced to (its q, less the post-increment of a
685+
* level that has signed the one beneath it).
686+
* @param targetQ the one-time key the index asks for.
687+
*/
688+
private static void checkNotRewound(int level, long currentQ, long targetQ)
689+
{
690+
if (targetQ < currentQ)
691+
{
692+
throw new IllegalArgumentException("HSS private key index would move level " + level
693+
+ " back from one-time key " + currentQ + " to " + targetQ);
694+
}
695+
}
696+
671697
public synchronized HSSPublicKeyParameters getPublicKey()
672698
{
673699
return new HSSPublicKeyParameters(l, getRootKey().getPublicKey());

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

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,11 +1411,12 @@ public void testSingleLevelWrapKeepsTheKey()
14111411
assertSame("the wrap regenerated an advanced root key", lms, advanced.getRootKey());
14121412
assertEquals("the wrap moved the index", 3, advanced.getIndex());
14131413

1414-
// the reset itself still works: asked for a different position, it does reposition
1415-
HSSPrivateKeyParameters moved = new HSSPrivateKeyParameters(lms, 1, 1 << sigParams.getH());
1414+
// the reset itself still works: asked for a different (later - see
1415+
// testResetKeyToIndexRefusesToRewind) position, it does reposition
1416+
HSSPrivateKeyParameters moved = new HSSPrivateKeyParameters(lms, 4, 1 << sigParams.getH());
14161417

14171418
assertNotSame("the reset failed to reposition to a different index", lms, moved.getRootKey());
1418-
assertEquals(1, moved.getRootKey().getIndex());
1419+
assertEquals(4, moved.getRootKey().getIndex());
14191420

14201421
// the Merkle tree is a function of I, the seed and the parameters and not of q, so the
14211422
// repositioned key is entitled to the tree it was built from rather than a rebuild costing
@@ -1439,6 +1440,87 @@ public void testSingleLevelWrapKeepsTheKey()
14391440
assertTrue("wrapped key produced a signature that does not verify", verifier.verifySignature(msg, sig));
14401441
}
14411442

1443+
/**
1444+
* A component key whose identifier and seed are unchanged is the same tree; an index that would
1445+
* move it back within that tree asks for one-time keys already used, and is refused. Forward
1446+
* moves still reposition.
1447+
*/
1448+
public void testResetKeyToIndexRefusesToRewind()
1449+
throws Exception
1450+
{
1451+
LMSigParameters sigParams = LMSigParameters.lms_sha256_n32_h5;
1452+
LMOtsParameters otsParams = LMOtsParameters.sha256_n32_w2;
1453+
int twoToH = 1 << sigParams.getH();
1454+
byte[] msg = Hex.decode("48656c6c6f");
1455+
1456+
// single level: the root is the last level and reads its q directly
1457+
LMSKeyPairGenerator gen = new LMSKeyPairGenerator();
1458+
gen.init(new LMSKeyGenerationParameters(LMSParameters.create(sigParams, otsParams), new SecureRandom()));
1459+
LMSPrivateKeyParameters lms = (LMSPrivateKeyParameters)gen.generateKeyPair().getPrivate();
1460+
LMSSigner lmsSigner = new LMSSigner();
1461+
lmsSigner.init(true, lms);
1462+
for (int i = 0; i < 3; ++i)
1463+
{
1464+
lmsSigner.generateSignature(msg);
1465+
}
1466+
assertEquals(3, lms.getIndex());
1467+
1468+
expectRewindRefused(lms, 2, twoToH);
1469+
assertEquals(3, new HSSPrivateKeyParameters(lms, 3, twoToH).getIndex());
1470+
assertEquals(4, new HSSPrivateKeyParameters(lms, 4, twoToH).getKeys().get(0).getIndex());
1471+
1472+
// two levels: the root is post-incremented past the child it signed, the bottom reads its q directly
1473+
HSSPrivateKeyParameters hss = HSSPrivateKeyParameters.generate(new HSSKeyGenerationParameters(
1474+
new LMSParameters[]{ LMSParameters.create(sigParams, otsParams), LMSParameters.create(sigParams, otsParams) },
1475+
new SecureRandom()));
1476+
for (int i = 0; i < twoToH + 1; ++i)
1477+
{
1478+
sign(hss, msg);
1479+
}
1480+
List<LMSPrivateKeyParameters> keys = hss.getKeys();
1481+
List<LMSSignature> sig = hss.getSig();
1482+
long limit = (long)twoToH * twoToH;
1483+
assertEquals(2, keys.get(0).getIndex());
1484+
assertEquals(1, keys.get(1).getIndex());
1485+
1486+
// back one leaf within the current bottom tree
1487+
expectRewindRefused(2, keys, sig, twoToH, limit);
1488+
// back into the previous bottom tree, which the root has already signed and moved past
1489+
expectRewindRefused(2, keys, sig, 5, limit);
1490+
// the position the keys are at, and one further on, are both fine
1491+
assertEquals(twoToH + 1, new HSSPrivateKeyParameters(2, keys, sig, twoToH + 1, limit).getIndex());
1492+
HSSPrivateKeyParameters forward = new HSSPrivateKeyParameters(2, keys, sig, twoToH + 8, limit);
1493+
assertEquals(8, forward.getKeys().get(1).getIndex());
1494+
assertTrue(verify(hss.getPublicKey(), sign(forward, msg), msg));
1495+
}
1496+
1497+
private static void expectRewindRefused(LMSPrivateKeyParameters lms, long index, long indexLimit)
1498+
{
1499+
try
1500+
{
1501+
new HSSPrivateKeyParameters(lms, index, indexLimit);
1502+
fail("index " + index + " rewound the key");
1503+
}
1504+
catch (IllegalArgumentException e)
1505+
{
1506+
assertTrue(e.getMessage(), e.getMessage().startsWith("HSS private key index would move level"));
1507+
}
1508+
}
1509+
1510+
private static void expectRewindRefused(int l, List<LMSPrivateKeyParameters> keys, List<LMSSignature> sig,
1511+
long index, long indexLimit)
1512+
{
1513+
try
1514+
{
1515+
new HSSPrivateKeyParameters(l, keys, sig, index, indexLimit);
1516+
fail("index " + index + " rewound the key");
1517+
}
1518+
catch (IllegalArgumentException e)
1519+
{
1520+
assertTrue(e.getMessage(), e.getMessage().startsWith("HSS private key index would move level"));
1521+
}
1522+
}
1523+
14421524
/**
14431525
* The HSS index and the bottom key's one-time index q are claimed together. They are two
14441526
* records of the same position - checkIndexAgainstKeys requires them to agree at decode - and

0 commit comments

Comments
 (0)