Skip to content

Commit dcaa1ae

Browse files
committed
LMS: equals/hashCode/concurrency hardening
1 parent 3eb62a0 commit dcaa1ae

3 files changed

Lines changed: 107 additions & 103 deletions

File tree

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

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ public class HSSPrivateKeyParameters
2929
private final long indexLimit;
3030
private long index = 0;
3131

32-
private HSSPublicKeyParameters publicKey;
33-
3432
public HSSPrivateKeyParameters(LMSPrivateKeyParameters key, long index, long indexLimit)
3533
{
3634
super(true);
@@ -53,8 +51,8 @@ public HSSPrivateKeyParameters(int l, List<LMSPrivateKeyParameters> keys, List<L
5351
super(true);
5452

5553
this.l = l;
56-
this.keys = Collections.unmodifiableList(keys);
57-
this.sig = Collections.unmodifiableList(sig);
54+
this.keys = Collections.unmodifiableList(new ArrayList<LMSPrivateKeyParameters>(keys));
55+
this.sig = Collections.unmodifiableList(new ArrayList<LMSSignature>(sig));
5856
this.index = index;
5957
this.indexLimit = indexLimit;
6058
this.isShard = false;
@@ -70,6 +68,8 @@ private HSSPrivateKeyParameters(int l, List<LMSPrivateKeyParameters> keys, List<
7068
super(true);
7169

7270
this.l = l;
71+
// No copy here, unlike the public constructor: the callers are extractKeyShard and
72+
// getInstance, which build fresh lists they do not retain.
7373
this.keys = Collections.unmodifiableList(keys);
7474
this.sig = Collections.unmodifiableList(sig);
7575
this.index = index;
@@ -98,8 +98,6 @@ public static HSSPrivateKeyParameters getInstance(byte[] privEnc, byte[] pubEnc)
9898
throw new IOException("HSS private key tree cache does not match the public key");
9999
}
100100

101-
pKey.publicKey = pubKey;
102-
103101
return pKey;
104102
}
105103

@@ -308,7 +306,7 @@ private static HSSPrivateKeyParameters makeCopy(HSSPrivateKeyParameters privateK
308306
}
309307
}
310308

311-
protected void updateHierarchy(LMSPrivateKeyParameters[] newKeys, LMSSignature[] newSig)
309+
private void updateHierarchy(LMSPrivateKeyParameters[] newKeys, LMSSignature[] newSig)
312310
{
313311
synchronized (this)
314312
{
@@ -341,7 +339,7 @@ public long getUsagesRemaining()
341339

342340
LMSPrivateKeyParameters getRootKey()
343341
{
344-
return keys.get(0);
342+
return getKeys().get(0);
345343
}
346344

347345
/**
@@ -396,11 +394,14 @@ synchronized List<LMSSignature> getSig()
396394

397395
/**
398396
* Reset to index will ensure that all LMS keys are correct for a given HSS index value.
399-
* Normally LMS keys updated in sync with their parent HSS key but in cases of sharding
397+
* Normally LMS keys are updated in sync with their parent HSS key but in cases of sharding
400398
* the normal monotonic updating does not apply and the state of the LMS keys needs to be
401399
* reset to match the current HSS index.
400+
* <p>
401+
* Should only be called under the monitor (lock) or during construction before the instance escapes.
402+
* </p>
402403
*/
403-
void resetKeyToIndex()
404+
private void resetKeyToIndex()
404405
{
405406
// Extract the original keys
406407
List<LMSPrivateKeyParameters> originalKeys = getKeys();
@@ -418,8 +419,8 @@ void resetKeyToIndex()
418419
}
419420

420421
boolean changed = false;
421-
LMSPrivateKeyParameters[] keys = originalKeys.toArray(new LMSPrivateKeyParameters[originalKeys.size()]);// new LMSPrivateKeyParameters[originalKeys.size()];
422-
LMSSignature[] sig = this.sig.toArray(new LMSSignature[this.sig.size()]);// new LMSSignature[originalKeys.size() - 1];
422+
LMSPrivateKeyParameters[] keys = originalKeys.toArray(new LMSPrivateKeyParameters[originalKeys.size()]);
423+
LMSSignature[] sig = this.sig.toArray(new LMSSignature[this.sig.size()]);
423424

424425
LMSPrivateKeyParameters originalRootKey = this.getRootKey();
425426

@@ -626,27 +627,40 @@ public boolean equals(Object o)
626627

627628
HSSPrivateKeyParameters that = (HSSPrivateKeyParameters)o;
628629

629-
if (l != that.l)
630-
{
631-
return false;
632-
}
633-
if (isShard != that.isShard)
634-
{
635-
return false;
636-
}
637-
if (indexLimit != that.indexLimit)
630+
if (this.l != that.l || this.isShard != that.isShard || this.indexLimit != that.indexLimit)
638631
{
639632
return false;
640633
}
641-
if (index != that.index)
634+
635+
//
636+
// index, keys and sig all move as consumed trees are replaced, and they move together -
637+
// replaceConsumedKey assigns keys and sig one after the other under this monitor - so read
638+
// each key's trio in one synchronized block to get a snapshot no unsynchronized reader
639+
// could tear. The lists are unmodifiable and replaced rather than mutated, so a captured
640+
// reference stays a coherent view after the lock drops. Neither monitor is held while the
641+
// other is taken, so a.equals(b) racing b.equals(a) cannot deadlock.
642+
//
643+
long thisIndex;
644+
List<LMSPrivateKeyParameters> thisKeys;
645+
List<LMSSignature> thisSig;
646+
synchronized (this)
642647
{
643-
return false;
648+
thisIndex = this.index;
649+
thisKeys = this.keys;
650+
thisSig = this.sig;
644651
}
645-
if (!keys.equals(that.keys))
652+
653+
long thatIndex;
654+
List<LMSPrivateKeyParameters> thatKeys;
655+
List<LMSSignature> thatSig;
656+
synchronized (that)
646657
{
647-
return false;
658+
thatIndex = that.index;
659+
thatKeys = that.keys;
660+
thatSig = that.sig;
648661
}
649-
return sig.equals(that.sig);
662+
663+
return thisIndex == thatIndex && thisKeys.equals(thatKeys) && thisSig.equals(thatSig);
650664
}
651665

652666
@Override
@@ -684,7 +698,20 @@ public synchronized byte[] getEncoded()
684698
@Override
685699
public int hashCode()
686700
{
687-
return getPublicKey().hashCode();
701+
//
702+
// Deliberately not getPublicKey().hashCode(): that reaches the root key's tree, which is
703+
// only built if the node cache does not already hold it - 2^h LM-OTS public keys from an
704+
// implicit call no caller expects to cost anything. The fields used here are the ones that
705+
// do not move as the key signs: the root key material is fixed (resetKeyToIndex only
706+
// repositions it, and LMSPrivateKeyParameters.hashCode is itself index-independent),
707+
// whereas index, keys and sig all change. Equal keys agree on all of these, so the
708+
// equals() contract holds.
709+
//
710+
int hc = l;
711+
hc = 31 * hc + (isShard ? 1 : 0);
712+
hc = 31 * hc + (int)(indexLimit ^ (indexLimit >>> 32));
713+
hc = 31 * hc + getRootKey().hashCode();
714+
return hc;
688715
}
689716

690717
@Override

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

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.bouncycastle.crypto.signers.lms.LMSEngine;
1616
import org.bouncycastle.util.Arrays;
1717
import org.bouncycastle.util.Exceptions;
18+
import org.bouncycastle.util.Objects;
1819
import org.bouncycastle.util.io.Streams;
1920

2021
public class LMSPrivateKeyParameters
@@ -40,7 +41,6 @@ public class LMSPrivateKeyParameters
4041
private final byte[] masterSecret;
4142
private final Map<CacheKey, byte[]> tCache;
4243
private final int maxCacheR;
43-
private final Digest tDigest;
4444

4545
private int q;
4646

@@ -62,7 +62,6 @@ public LMSPrivateKeyParameters(LMSigParameters lmsParameter, LMOtsParameters ots
6262
this.masterSecret = Arrays.clone(masterSecret);
6363
this.maxCacheR = 1 << (parameters.getH() + 1);
6464
this.tCache = new WeakHashMap<CacheKey, byte[]>();
65-
this.tDigest = LMSEngine.createDigest(lmsParameter);
6665
}
6766

6867
private LMSPrivateKeyParameters(LMSPrivateKeyParameters parent, int q, int maxQ)
@@ -76,7 +75,6 @@ private LMSPrivateKeyParameters(LMSPrivateKeyParameters parent, int q, int maxQ)
7675
this.masterSecret = parent.masterSecret;
7776
this.maxCacheR = 1 << parameters.getH();
7877
this.tCache = parent.tCache;
79-
this.tDigest = LMSEngine.createDigest(parameters);
8078
this.publicKey = parent.publicKey;
8179
}
8280

@@ -85,7 +83,10 @@ public static LMSPrivateKeyParameters getInstance(byte[] privEnc, byte[] pubEnc)
8583
{
8684
LMSPrivateKeyParameters pKey = getInstance(privEnc);
8785

88-
pKey.publicKey = LMSPublicKeyParameters.getInstance(pubEnc);
86+
synchronized (pKey)
87+
{
88+
pKey.publicKey = LMSPublicKeyParameters.getInstance(pubEnc);
89+
}
8990

9091
return pKey;
9192
}
@@ -291,14 +292,16 @@ private static void validateTreeCache(LMSPrivateKeyParameters key, byte[][] cach
291292
*/
292293
byte[][] deriveChildKey()
293294
{
295+
int q;
294296
synchronized (this)
295297
{
298+
q = this.q;
296299
if (q >= maxQ)
297300
{
298301
throw new ExhaustedPrivateKeyException("ots private key exhausted");
299302
}
300-
return LMSEngine.deriveChildKey(otsParameters, I, masterSecret, q);
301303
}
304+
return LMSEngine.deriveChildKey(otsParameters, I, masterSecret, q);
302305
}
303306

304307
/**
@@ -470,7 +473,11 @@ private byte[] findT(CacheKey key)
470473

471474
private byte[] calcT(int r)
472475
{
473-
int h = this.getSigParameters().getH();
476+
LMSigParameters sigParameters = getSigParameters();
477+
478+
Digest tDigest = LMSEngine.createDigest(sigParameters);
479+
480+
int h = sigParameters.getH();
474481

475482
int twoToh = 1 << h;
476483

@@ -554,43 +561,37 @@ public boolean equals(Object o)
554561

555562
LMSPrivateKeyParameters that = (LMSPrivateKeyParameters)o;
556563

557-
if (q != that.q)
558-
{
559-
return false;
560-
}
561-
if (maxQ != that.maxQ)
562-
{
563-
return false;
564-
}
565-
if (!Arrays.areEqual(I, that.I))
566-
{
567-
return false;
568-
}
569-
if (parameters != null ? !parameters.equals(that.parameters) : that.parameters != null)
570-
{
571-
return false;
572-
}
573-
if (otsParameters != null ? !otsParameters.equals(that.otsParameters) : that.otsParameters != null)
574-
{
575-
return false;
576-
}
577-
if (!Arrays.constantTimeAreEqual(masterSecret, that.masterSecret))
578-
{
579-
return false;
580-
}
581-
582-
return true;
564+
return this.getIndex() == that.getIndex()
565+
&& this.maxQ == that.maxQ
566+
&& Arrays.areEqual(this.I, that.I)
567+
&& Objects.areEqual(this.parameters, that.parameters)
568+
&& Objects.areEqual(this.otsParameters, that.otsParameters)
569+
&& Arrays.constantTimeAreEqual(this.masterSecret, that.masterSecret);
583570
}
584571

585572
@Override
586573
public int hashCode()
587574
{
588-
return getPublicKey().hashCode();
575+
//
576+
// Deliberately not getPublicKey().hashCode(): the root is only there if the tree cache
577+
// holds it, so on a freshly generated or decoded key that builds the whole Merkle tree -
578+
// 2^h LM-OTS public keys - from an implicit call no caller expects to cost anything. It is
579+
// also independent of q, so a key's hash does not move as it signs, and of the master
580+
// secret, so no function of the seed is handed out. Equal keys agree on every field used
581+
// here, so the equals() contract holds.
582+
//
583+
int hc = Objects.hashCode(parameters);
584+
hc = 31 * hc + Objects.hashCode(otsParameters);
585+
hc = 31 * hc + maxQ;
586+
hc = 31 * hc + Arrays.hashCode(I);
587+
return hc;
589588
}
590589

591590
public byte[] getEncoded()
592591
throws IOException
593592
{
593+
int q = getIndex();
594+
594595
//
595596
// NB there is no formal specification for the encoding of private keys.
596597
// It is implementation dependent.

0 commit comments

Comments
 (0)