Skip to content

Commit 743396d

Browse files
committed
Reject a field-zero divisor or multiplier in ShamirSplitSecret.divide and multiple, since the GF(256) reduction makes any multiple of 256 the field's zero element and would overwrite every share, in the base and jdk1.4 copies, and cover it with a regression test.
1 parent 8b7f5bb commit 743396d

4 files changed

Lines changed: 96 additions & 8 deletions

File tree

core/src/main/java/org/bouncycastle/crypto/threshold/ShamirSplitSecret.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ public ShamirSplitSecretShare[] getSecretShares()
4343
public ShamirSplitSecret multiple(int mul)
4444
throws IOException
4545
{
46+
// gfMul reduces the multiplier modulo 256, so any mul with (mul & 0xFF) == 0 is the field's
47+
// zero element, which would rewrite every share in place to zero and irreversibly destroy the
48+
// secret. The multiplier is a caller parameter rather than secret material, so rejecting it up
49+
// front leaks nothing.
50+
if ((mul & 0xFF) == 0)
51+
{
52+
throw new IllegalArgumentException("Invalid input: the multiplier cannot be zero.");
53+
}
54+
4655
byte[] ss;
4756
for (int i = 0; i < secretShares.length; ++i)
4857
{
@@ -59,10 +68,12 @@ public ShamirSplitSecret multiple(int mul)
5968
public ShamirSplitSecret divide(int div)
6069
throws IOException
6170
{
62-
// division by zero is undefined in the field, and every share is rewritten in place, so
63-
// accepting it would silently overwrite the whole set with zeroes. The divisor is a caller
64-
// parameter rather than secret material, so rejecting it up front leaks nothing.
65-
if (div == 0)
71+
// gfDiv/gfMul reduce the operand modulo 256, so any divisor with (div & 0xFF) == 0 - zero, or
72+
// a multiple of 256 - is the field's zero element. Division by zero is undefined, and every
73+
// share is rewritten in place, so accepting it would silently overwrite the whole set with
74+
// zeroes. The divisor is a caller parameter rather than secret material, so rejecting it up
75+
// front leaks nothing.
76+
if ((div & 0xFF) == 0)
6677
{
6778
throw new IllegalArgumentException("Invalid input: the divisor cannot be zero.");
6879
}

core/src/main/jdk1.4/org/bouncycastle/crypto/threshold/ShamirSplitSecret.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ public SecretShare[] getSecretShares()
4343
public ShamirSplitSecret multiple(int mul)
4444
throws IOException
4545
{
46+
// gfMul reduces the multiplier modulo 256, so any mul with (mul & 0xFF) == 0 is the field's
47+
// zero element, which would rewrite every share in place to zero and irreversibly destroy the
48+
// secret. The multiplier is a caller parameter rather than secret material, so rejecting it up
49+
// front leaks nothing.
50+
if ((mul & 0xFF) == 0)
51+
{
52+
throw new IllegalArgumentException("Invalid input: the multiplier cannot be zero.");
53+
}
54+
4655
byte[] ss;
4756
for (int i = 0; i < secretShares.length; ++i)
4857
{
@@ -59,10 +68,12 @@ public ShamirSplitSecret multiple(int mul)
5968
public ShamirSplitSecret divide(int div)
6069
throws IOException
6170
{
62-
// division by zero is undefined in the field, and every share is rewritten in place, so
63-
// accepting it would silently overwrite the whole set with zeroes. The divisor is a caller
64-
// parameter rather than secret material, so rejecting it up front leaks nothing.
65-
if (div == 0)
71+
// gfDiv/gfMul reduce the operand modulo 256, so any divisor with (div & 0xFF) == 0 - zero, or
72+
// a multiple of 256 - is the field's zero element. Division by zero is undefined, and every
73+
// share is rewritten in place, so accepting it would silently overwrite the whole set with
74+
// zeroes. The divisor is a caller parameter rather than secret material, so rejecting it up
75+
// front leaks nothing.
76+
if ((div & 0xFF) == 0)
6677
{
6778
throw new IllegalArgumentException("Invalid input: the divisor cannot be zero.");
6879
}

core/src/test/java/org/bouncycastle/crypto/threshold/test/ShamirSecretSplitterTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public void performTest()
3232
{
3333
testShamirSecretResplit();
3434
testShamirSecretMultipleDivide();
35+
testMultipleDivideRejectFieldZero();
3536
testShamirSecretSplitterSplitAround();
3637
testPolynomial();
3738
testPolynomialModeEquivalence();
@@ -437,6 +438,70 @@ public void testShamirSecretMultipleDivide()
437438
assertFalse(Arrays.areEqual(secret1, secret3));
438439
}
439440

441+
/**
442+
* divide() and multiple() rewrite every share in place, so a zero divisor or multiplier would
443+
* silently overwrite the whole set with zeroes and irreversibly destroy the secret. gfMul reduces
444+
* its operand modulo 256, so the field's zero element is any value with (v &amp; 0xFF) == 0 - not just
445+
* an exact 0 - and both methods reject it up front. An in-range re-scale must still be accepted.
446+
*/
447+
public void testMultipleDivideRejectFieldZero()
448+
throws IOException
449+
{
450+
int l = 9, m = 3, n = 9;
451+
SecureRandom random = new SecureRandom();
452+
ShamirSecretSplitter.Algorithm algorithm = ShamirSecretSplitter.Algorithm.AES;
453+
454+
int[] fieldZeros = new int[]{0, 256, 512};
455+
456+
for (int z = 0; z != fieldZeros.length; z++)
457+
{
458+
ShamirSecretSplitter splitter = ShamirSecretSplitter.getInstance(algorithm, l, random);
459+
460+
try
461+
{
462+
((ShamirSplitSecret)splitter.split(m, n)).divide(fieldZeros[z]);
463+
fail("divide accepted the field-zero divisor " + fieldZeros[z]);
464+
}
465+
catch (IllegalArgumentException e)
466+
{
467+
assertEquals("Invalid input: the divisor cannot be zero.", e.getMessage());
468+
}
469+
470+
try
471+
{
472+
((ShamirSplitSecret)splitter.split(m, n)).multiple(fieldZeros[z]);
473+
fail("multiple accepted the field-zero multiplier " + fieldZeros[z]);
474+
}
475+
catch (IllegalArgumentException e)
476+
{
477+
assertEquals("Invalid input: the multiplier cannot be zero.", e.getMessage());
478+
}
479+
}
480+
481+
// an in-range multiplier is still accepted, and multiple(k) followed by divide(k) leaves every
482+
// share byte unchanged - the guard must not reject a legitimate re-scale.
483+
ShamirSecretSplitter splitter = ShamirSecretSplitter.getInstance(algorithm, l, random);
484+
ShamirSplitSecret splitSecret = (ShamirSplitSecret)splitter.split(m, n);
485+
486+
ShamirSplitSecretShare[] originalShares = (ShamirSplitSecretShare[])splitSecret.getSecretShares();
487+
byte[][] before = new byte[originalShares.length][];
488+
for (int i = 0; i != originalShares.length; i++)
489+
{
490+
before[i] = Arrays.clone(originalShares[i].getEncoded());
491+
}
492+
493+
int k = random.nextInt(255) + 1; // 1..255, a non-zero field element
494+
splitSecret.multiple(k).divide(k);
495+
496+
ShamirSplitSecretShare[] after = (ShamirSplitSecretShare[])splitSecret.getSecretShares();
497+
assertEquals("share count changed", before.length, after.length);
498+
for (int i = 0; i != before.length; i++)
499+
{
500+
assertTrue("multiple(" + k + ").divide(" + k + ") altered share " + i,
501+
Arrays.areEqual(before[i], after[i].getEncoded()));
502+
}
503+
}
504+
440505
public void testShamirSecretSplitterSplitAround()
441506
throws IOException
442507
{

docs/releasenotes.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ <h3>2.1.2 Defects Fixed</h3>
107107
<li>Neither copy of PKIXCertPathReviewer - org.bouncycastle.pkix.jcajce.PKIXCertPathReviewer nor the legacy org.bouncycastle.x509.PKIXCertPathReviewer - applied X.509 name constraints to the end-entity certificate. checkNameConstraints() walked the path with a loop bound of index &gt; 0, which is the bound the CA-only steps want, but index 0 is the target certificate under the standard CertPath ordering, so the permitted and excluded subtree checks of RFC 5280 sec. 6.1.3 (b) and (c) never ran against the leaf's subject DN or its subjectAltName. A chain whose leaf violated a NameConstraints extension imposed by its own issuing CA therefore reported isValidCertPath() true with an empty error list, while CertPathValidator.getInstance("PKIX", "BC") - which shares no code with the reviewer and iterates index &gt;= 0 through RFC3280CertPathUtilities.processCertBC - rejected the identical chain against the identical trust anchor. An application using the reviewer to make the trust decision, rather than for its per-certificate diagnostics alongside a real validation, accepted a certificate the constrained CA was never authorised to issue. Both copies now check every certificate in the path including the target, waive the sec. 4.2.1.10 self-issued exemption for the final certificate as sec. 6.1.3 requires, and skip the sec. 6.1.4 (g) constraint-accumulation step for the target, where there is no next certificate to accumulate for.</li>
108108
<li>The MLS (RFC 9420) external-commit path let a joiner remove an arbitrary existing group member. Validation of an external commit's proposal list, org.bouncycastle.mls.protocol.Group.validateExternalCachedProposals, counted the proposals by type and bounded the removed leaf index, but never established that the removed leaf had anything to do with the joiner. RFC 9420 sec. 12.2 permits "at most one Remove proposal, with which the joiner removes an old version of themselves", and requires that where one is present the LeafNode in the commit's path field meet the criteria it would have to meet in an Update for the removed leaf - in particular that its credential present identifiers acceptable for the removed participant. The ordinary proposal-list validator's self-remove rule is deliberately not applied on this path, because a resync commit legitimately removes a leaf the joiner owns, but nothing was put in its place, so any party holding the group's public GroupInfo - which is precisely what an external joiner is meant to be given - could commit a Remove naming any member's LeafIndex and have every member apply it, evicting that member and taking over their slot in the ratchet tree. The credential check that should have prevented this existed only in the gRPC interop harness (MLSClientImpl.externalJoinImpl), not in Group itself, so it protected no other caller of the public Group.externalJoin / Group.handle API. An external commit carrying a Remove is now accepted only when the removed leaf's credential is identical to the one in the joiner's own new leaf, on both the sending and receiving side. Credentials are compared by their encoding rather than by Credential.getIdentity(), which is populated only for the basic credential type and would have matched any two x509 credentials against each other.</li>
109109
<li>The high-level OpenPGP message API offered no way to bound how far a compressed data packet expands. A compressed data packet declares no decompressed length, so a small packet can expand into an arbitrarily large amount of data; PGPCompressedData has long carried getDataStream(long) for exactly this, which stops the decompression itself once the limit is passed, but org.bouncycastle.openpgp.api.OpenPGPMessageInputStream always called the unbounded getDataStream() overload and OpenPGPPolicy exposed no corresponding property, so a caller on the recommended OpenPGPMessageProcessor path had no way to set one. OpenPGPMessageInputStream.MAX_RECURSION bounds how many compression layers may nest, which is a separate limit and says nothing about how much any one layer may produce; and a consumer counting bytes off the returned stream cannot help itself, because the decompression producing those bytes has already happened by the time it can act. OpenPGPPolicy has gained getMaximumDecompressedDataSize(), settable on OpenPGPDefaultPolicy via setMaximumDecompressedDataSize(long), which OpenPGPMessageInputStream now applies per compressed data packet; passing it raises a StreamOverflowException out of the message input stream. As on the low-level pair the default is unbounded, so existing behaviour is unchanged and choosing a limit remains the application's decision. OpenPGPMessageProcessor also gained an internal accessor for the policy it was configured with, since the two-argument constructor's policy is not the one getImplementation().policy() returns.</li>
110+
<li>org.bouncycastle.crypto.threshold.ShamirSplitSecret.divide(int) and multiple(int), which re-scale an existing share set in place, could silently overwrite every share with zeroes and irreversibly destroy the secret. GF(256) multiplication reduces its operand modulo 256, so a divisor or multiplier whose low eight bits are zero is the field's zero element: divide() rejected only an exactly-zero divisor (div == 0) and so let a non-zero multiple of 256 through to act as division by zero, and multiple() performed no such check at all, so multiple(0) - or any multiple of 256 - rewrote each share byte to zero through gfMul(x, 0). Both methods now reject a divisor or multiplier with (value &amp; 0xFF) == 0 up front with an IllegalArgumentException; the value is a caller parameter rather than secret material, so the check leaks nothing. In-range re-scaling is unaffected.</li>
110111
</ul>
111112

112113
<h3>2.1.3 Additional Features and Functionality</h3>

0 commit comments

Comments
 (0)