Skip to content

Commit a70fa22

Browse files
committed
Compare the SRP-6a evidence messages and the J-PAKE MacTag in constant time over fixed-width encodings rather than with BigInteger.equals, keeping each expected value in the digest's own output form so its length cannot vary with the secret, and sign-extending the J-PAKE tag, which is read signed, incorporating github PR #2406, relates to github #2406.
1 parent e0dce82 commit a70fa22

11 files changed

Lines changed: 319 additions & 25 deletions

File tree

CONTRIBUTORS.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@
594594
<li>subbudvk &lt;https://github.com/subbudvk&gt; - initial author on S2K parser hardening work for OpenPGP API.</li>
595595
<li>mkarasik &lt;https://github.com/mkarasik&gt; - initial work on EST server-side key generation (RFC 7030 4.4).</li>
596596
<li>Bernd Pr&uuml;nster (A-SIT Plus) &lt;bernd.pruenster&#064;a-sit.at&gt; - reported lenient ASN.1 UTCTime/GeneralizedTime parsing accepting structurally malformed content, with fuzzing-derived test cases.</li>
597-
<li>Naveed Khan &lt;https://github.com/rootvector2&gt; - constant time comparison of membership and confirmation tags in the MLS API. Reported unbounded array allocation in the BKS/UBER keystore load path (OutOfMemoryError DoS from a crafted keystore) and introduction of a capped decompression limit in PGPCompressedData.getDataStream(), both with POC. Original submission creating S/MIME backing temp files with owner-only permissions (PR #2326). Rejecting empty sequences in the X.509 extension parsers whose RFC 5280 syntax is SEQUENCE SIZE (1..MAX) - CRLDistPoint, CertificatePolicies, ExtendedKeyUsage, PolicyMappings, and SubjectDirectoryAttributes (PR #2331). Hardening asn1.cms.SignerInfo decode to reject a non-INTEGER version or non-tagged unsignedAttrs via getInstance rather than leaking a ClassCastException (PR #2342). Fixing an off-by-4 header-length guard in the OpenPGP NotationData signature subpacket parser (PR #2346). Rejecting CR/LF in the S/MIME streaming writer header names and values (SMIMEEnvelopedWriter/SMIMESignedWriter withHeader) to prevent MIME header injection (PR #2348). Adding minimum-length guards to the SM2 decrypt and GOST28147/DSTU7624/DESede/RC2 key-wrap unwrap paths (PR #2359). Guarding the ECDH session-key length in the JCE OpenPGP decryptor (PR #2383). Fixing the identity fast-path type guards in the OER getInstance factories and the transposed isInstance arguments in OEROptional.getObject (PR #2373). Requiring the signature handed to AIMerSigner.verifySignature to be exactly the parameter set's signature size (PR #2401).</li>
597+
<li>Naveed Khan &lt;https://github.com/rootvector2&gt; - constant time comparison of membership and confirmation tags in the MLS API. Reported unbounded array allocation in the BKS/UBER keystore load path (OutOfMemoryError DoS from a crafted keystore) and introduction of a capped decompression limit in PGPCompressedData.getDataStream(), both with POC. Original submission creating S/MIME backing temp files with owner-only permissions (PR #2326). Rejecting empty sequences in the X.509 extension parsers whose RFC 5280 syntax is SEQUENCE SIZE (1..MAX) - CRLDistPoint, CertificatePolicies, ExtendedKeyUsage, PolicyMappings, and SubjectDirectoryAttributes (PR #2331). Hardening asn1.cms.SignerInfo decode to reject a non-INTEGER version or non-tagged unsignedAttrs via getInstance rather than leaking a ClassCastException (PR #2342). Fixing an off-by-4 header-length guard in the OpenPGP NotationData signature subpacket parser (PR #2346). Rejecting CR/LF in the S/MIME streaming writer header names and values (SMIMEEnvelopedWriter/SMIMESignedWriter withHeader) to prevent MIME header injection (PR #2348). Adding minimum-length guards to the SM2 decrypt and GOST28147/DSTU7624/DESede/RC2 key-wrap unwrap paths (PR #2359). Guarding the ECDH session-key length in the JCE OpenPGP decryptor (PR #2383). Fixing the identity fast-path type guards in the OER getInstance factories and the transposed isInstance arguments in OEROptional.getObject (PR #2373). Requiring the signature handed to AIMerSigner.verifySignature to be exactly the parameter set's signature size (PR #2401). Constant time comparison of the SRP-6a evidence messages (PR #2406).</li>
598598
<li>suraj0208 &lt;https://github.com/suraj0208&gt; - initial work on auto-detecting private key reader (JcaPrivateKeyReader).</li>
599599
<li>liamgilligan &lt;https://github.com/liamgilligan&gt; - noticing the BIP-340 step numbering in the BIP340Signer signing comments was incorrect (PR #2340).</li>
600600
<li>digi-scrypt &lt;https://github.com/digi-scrypt&gt; - disabling DTD and external-entity resolution in KMIPInputStream to close an XXE (local file disclosure / SSRF) exposure in KMIP XML parsing (PR #2315).</li>

core/src/main/java/org/bouncycastle/crypto/agreement/jpake/JPAKEUtil.java

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,25 @@ public static BigInteger calculateMacTag(
460460
BigInteger gx4,
461461
BigInteger keyingMaterial,
462462
Digest digest)
463+
{
464+
return new BigInteger(calculateMacTagEncoded(
465+
participantId, partnerParticipantId, gx1, gx2, gx3, gx4, keyingMaterial, digest));
466+
}
467+
468+
/**
469+
* As {@link #calculateMacTag}, but leaving the result in the MAC's own fixed-width output form
470+
* for {@link #constantTimeEquals}. Read back as a BigInteger the encoding is minimal, so its
471+
* length would vary with the value.
472+
*/
473+
private static byte[] calculateMacTagEncoded(
474+
String participantId,
475+
String partnerParticipantId,
476+
BigInteger gx1,
477+
BigInteger gx2,
478+
BigInteger gx3,
479+
BigInteger gx4,
480+
BigInteger keyingMaterial,
481+
Digest digest)
463482
{
464483
byte[] macKey = calculateMacKey(
465484
keyingMaterial,
@@ -484,8 +503,7 @@ public static BigInteger calculateMacTag(
484503

485504
Arrays.fill(macKey, (byte)0);
486505

487-
return new BigInteger(macOutput);
488-
506+
return macOutput;
489507
}
490508

491509
/**
@@ -537,7 +555,7 @@ public static void validateMacTag(
537555
* x1 <-> x3
538556
* x2 <-> x4
539557
*/
540-
BigInteger expectedMacTag = calculateMacTag(
558+
byte[] expectedMacTag = calculateMacTagEncoded(
541559
partnerParticipantId,
542560
participantId,
543561
gx3,
@@ -547,14 +565,56 @@ public static void validateMacTag(
547565
keyingMaterial,
548566
digest);
549567

550-
if (!Arrays.constantTimeAreEqual(expectedMacTag.toByteArray(), partnerMacTag.toByteArray()))
568+
if (!constantTimeEquals(expectedMacTag, partnerMacTag))
551569
{
552570
throw new CryptoException(
553571
"Partner MacTag validation failed. "
554572
+ "Therefore, the password, MAC, or digest algorithm of each participant does not match.");
555573
}
556574
}
557575

576+
/**
577+
* Constant-time comparison of the partner's MacTag against the expected one.
578+
* <p>
579+
* The expected value is kept in the MAC's own output form so the comparison runs over a fixed
580+
* number of bytes; read back as a BigInteger its encoding is minimal, so the length alone would
581+
* vary with the secret-derived value. Note calculateMacTag reads the MAC output with the
582+
* <i>signed</i> BigInteger(byte[]) constructor, so a MacTag is negative about half the time and
583+
* the supplied value has to be encoded two's-complement and sign-extended to match:
584+
* BigIntegers.asUnsignedByteArray zero-pads instead, which does not round-trip a negative tag.
585+
* A value too wide to be a MacTag cannot match and is rejected before any comparison, a
586+
* decision taken purely on what the partner sent.
587+
*
588+
* @param expectedEnc the locally computed MacTag, as the MAC produced it.
589+
* @param supplied the MacTag received from the partner.
590+
* @return true if the two are equal.
591+
*/
592+
private static boolean constantTimeEquals(byte[] expectedEnc, BigInteger supplied)
593+
{
594+
int length = expectedEnc.length;
595+
596+
if (supplied.bitLength() >= length * 8)
597+
{
598+
return false;
599+
}
600+
601+
byte[] suppliedEnc = new byte[length];
602+
if (supplied.signum() < 0)
603+
{
604+
Arrays.fill(suppliedEnc, (byte)0xFF);
605+
}
606+
607+
byte[] minimal = supplied.toByteArray();
608+
System.arraycopy(minimal, 0, suppliedEnc, length - minimal.length, minimal.length);
609+
610+
boolean rv = Arrays.constantTimeAreEqual(expectedEnc, suppliedEnc);
611+
612+
Arrays.fill(minimal, (byte)0);
613+
Arrays.fill(suppliedEnc, (byte)0);
614+
615+
return rv;
616+
}
617+
558618
private static void updateDigest(Digest digest, BigInteger bigInteger)
559619
{
560620
byte[] byteArray = BigIntegers.asUnsignedByteArray(bigInteger);

core/src/main/java/org/bouncycastle/crypto/agreement/srp/SRP6Client.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ public boolean verifyServerEvidenceMessage(BigInteger serverM2) throws CryptoExc
153153
}
154154

155155
// Compute the own server evidence message 'M2'
156-
BigInteger computedM2 = SRP6Util.calculateM2(digest, N, A, M1, S);
157-
if (computedM2.equals(serverM2))
156+
byte[] computedM2 = SRP6Util.calculateM2Encoded(digest, N, A, M1, S);
157+
if (SRP6Util.constantTimeEquals(computedM2, serverM2))
158158
{
159159
this.M2 = serverM2;
160160
return true;

core/src/main/java/org/bouncycastle/crypto/agreement/srp/SRP6Server.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ public boolean verifyClientEvidenceMessage(BigInteger clientM1) throws CryptoExc
132132
}
133133

134134
// Compute the own client evidence message 'M1'
135-
BigInteger computedM1 = SRP6Util.calculateM1(digest, N, A, B, S);
136-
if (computedM1.equals(clientM1))
135+
byte[] computedM1 = SRP6Util.calculateM1Encoded(digest, N, A, B, S);
136+
if (SRP6Util.constantTimeEquals(computedM1, clientM1))
137137
{
138138
this.M1 = clientM1;
139139
return true;

core/src/main/java/org/bouncycastle/crypto/agreement/srp/SRP6Util.java

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import org.bouncycastle.crypto.CryptoException;
77
import org.bouncycastle.crypto.Digest;
8+
import org.bouncycastle.util.Arrays;
89
import org.bouncycastle.util.BigIntegers;
910

1011
public class SRP6Util
@@ -74,8 +75,7 @@ public static BigInteger validatePublicValue(BigInteger N, BigInteger val)
7475
*/
7576
public static BigInteger calculateM1(Digest digest, BigInteger N, BigInteger A, BigInteger B, BigInteger S)
7677
{
77-
BigInteger M1 = hashPaddedTriplet(digest, N, A, B, S);
78-
return M1;
78+
return new BigInteger(1, calculateM1Encoded(digest, N, A, B, S));
7979
}
8080

8181
/**
@@ -91,8 +91,7 @@ public static BigInteger calculateM1(Digest digest, BigInteger N, BigInteger A,
9191
*/
9292
public static BigInteger calculateM2(Digest digest, BigInteger N, BigInteger A, BigInteger M1, BigInteger S)
9393
{
94-
BigInteger M2 = hashPaddedTriplet(digest, N, A, M1, S);
95-
return M2;
94+
return new BigInteger(1, calculateM2Encoded(digest, N, A, M1, S));
9695
}
9796

9897
/**
@@ -114,7 +113,25 @@ public static BigInteger calculateKey(Digest digest, BigInteger N, BigInteger S)
114113
return new BigInteger(1, output);
115114
}
116115

117-
private static BigInteger hashPaddedTriplet(Digest digest, BigInteger N, BigInteger n1, BigInteger n2, BigInteger n3)
116+
/**
117+
* As {@link #calculateM1(Digest, BigInteger, BigInteger, BigInteger, BigInteger)}, but leaving
118+
* the result in the digest's own fixed-width output form for {@link #constantTimeEquals}.
119+
*/
120+
static byte[] calculateM1Encoded(Digest digest, BigInteger N, BigInteger A, BigInteger B, BigInteger S)
121+
{
122+
return hashPaddedTriplet(digest, N, A, B, S);
123+
}
124+
125+
/**
126+
* As {@link #calculateM2(Digest, BigInteger, BigInteger, BigInteger, BigInteger)}, but leaving
127+
* the result in the digest's own fixed-width output form for {@link #constantTimeEquals}.
128+
*/
129+
static byte[] calculateM2Encoded(Digest digest, BigInteger N, BigInteger A, BigInteger M1, BigInteger S)
130+
{
131+
return hashPaddedTriplet(digest, N, A, M1, S);
132+
}
133+
134+
private static byte[] hashPaddedTriplet(Digest digest, BigInteger N, BigInteger n1, BigInteger n2, BigInteger n3)
118135
{
119136
int padLength = (N.bitLength() + 7) / 8;
120137

@@ -129,7 +146,7 @@ private static BigInteger hashPaddedTriplet(Digest digest, BigInteger N, BigInte
129146
byte[] output = new byte[digest.getDigestSize()];
130147
digest.doFinal(output, 0);
131148

132-
return new BigInteger(1, output);
149+
return output;
133150
}
134151

135152
private static BigInteger hashPaddedPair(Digest digest, BigInteger N, BigInteger n1, BigInteger n2)
@@ -159,4 +176,35 @@ private static byte[] getPadded(BigInteger n, int length)
159176
}
160177
return bs;
161178
}
179+
180+
/**
181+
* Constant-time comparison of an evidence message received from the peer against the locally
182+
* computed one.
183+
* <p>
184+
* The expected value is kept in its raw digest-output form so the comparison runs over a fixed
185+
* number of bytes: read back as a BigInteger its encoding is minimal, so the length alone would
186+
* vary with the secret-derived value. The supplied value is a non-negative digest output too,
187+
* so one that is negative or too large to be one cannot match and is rejected before any
188+
* comparison - a decision taken purely on what the peer sent, which reveals nothing.
189+
*
190+
* @param expectedEnc the locally computed evidence message, as the digest produced it.
191+
* @param supplied the evidence message received from the peer.
192+
* @return true if the two are equal.
193+
*/
194+
static boolean constantTimeEquals(byte[] expectedEnc, BigInteger supplied)
195+
{
196+
if (supplied.signum() < 0 || supplied.bitLength() > expectedEnc.length * 8)
197+
{
198+
return false;
199+
}
200+
201+
byte[] suppliedEnc = BigIntegers.asUnsignedByteArray(expectedEnc.length, supplied);
202+
203+
boolean rv = Arrays.constantTimeAreEqual(expectedEnc, suppliedEnc);
204+
205+
Arrays.fill(suppliedEnc, (byte)0);
206+
207+
return rv;
208+
}
209+
162210
}

core/src/test/java/org/bouncycastle/crypto/agreement/test/JPAKEUtilTest.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,62 @@ public void testValidateMacTag()
139139
}
140140
}
141141

142+
/**
143+
* The MacTag comparison encodes both sides to the MAC's fixed width so its running time does
144+
* not depend on the expected - secret-derived - value. calculateMacTag reads the MAC output
145+
* with the <i>signed</i> BigInteger(byte[]) constructor, so a tag is negative about half the
146+
* time, and about one tag in 240 has a two's-complement encoding shorter than the MAC. Those
147+
* are the tags a zero-padding fixed-width encoding (BigIntegers.asUnsignedByteArray) fails to
148+
* round-trip, so enough tags are exercised here to include some, and every one of them has to
149+
* validate. A tag too wide to be one is rejected.
150+
*/
151+
public void testValidateMacTagEncoding()
152+
throws CryptoException
153+
{
154+
JPAKEPrimeOrderGroup pg1 = JPAKEPrimeOrderGroups.SUN_JCE_1024;
155+
156+
Digest digest = SHA256Digest.newInstance();
157+
158+
BigInteger gx1 = BigInteger.valueOf(11);
159+
BigInteger gx2 = BigInteger.valueOf(22);
160+
BigInteger gx3 = BigInteger.valueOf(33);
161+
BigInteger gx4 = BigInteger.valueOf(44);
162+
163+
int shortEncodings = 0;
164+
165+
for (int i = 0; i != 4096; i++)
166+
{
167+
BigInteger keyingMaterial = BigInteger.valueOf(i + 1);
168+
169+
BigInteger macTag = JPAKEUtil.calculateMacTag(
170+
"participantId", "partnerParticipantId", gx1, gx2, gx3, gx4, keyingMaterial, digest);
171+
172+
if (macTag.toByteArray().length != digest.getDigestSize())
173+
{
174+
shortEncodings++;
175+
}
176+
177+
/* the tag the partner computed must always validate */
178+
JPAKEUtil.validateMacTag("partnerParticipantId", "participantId",
179+
gx3, gx4, gx1, gx2, keyingMaterial, digest, macTag);
180+
181+
/* and a tag too wide to be one must not */
182+
try
183+
{
184+
JPAKEUtil.validateMacTag("partnerParticipantId", "participantId",
185+
gx3, gx4, gx1, gx2, keyingMaterial, digest,
186+
macTag.add(BigInteger.ONE.shiftLeft(8 * digest.getDigestSize())));
187+
fail("oversized MacTag accepted");
188+
}
189+
catch (CryptoException e)
190+
{
191+
// pass
192+
}
193+
}
194+
195+
assertTrue("no short-encoding MacTags were exercised", shortEncodings > 0);
196+
}
197+
142198
public void testValidateNotNull()
143199
{
144200
JPAKEUtil.validateNotNull("a", "description");

core/src/test/java/org/bouncycastle/crypto/test/SRP6Test.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public void performTest() throws Exception
3939
rfc5054AppendixBTestVectors();
4040

4141
testMutualVerification(SRP6StandardGroups.rfc5054_1024);
42+
testEvidenceMessageVerification(SRP6StandardGroups.rfc5054_1024);
4243
testClientCatchesBadB(SRP6StandardGroups.rfc5054_1024);
4344
testServerCatchesBadA(SRP6StandardGroups.rfc5054_1024);
4445

@@ -288,6 +289,94 @@ private void testMutualVerification(SRP6GroupParameters group) throws CryptoExce
288289
}
289290
}
290291

292+
/**
293+
* Exercises the evidence-message (M1/M2) verification path, which is where the received
294+
* keyed authenticators are checked. A correct M1/M2 must be accepted and a tampered one
295+
* rejected, whether the tampering leaves the encoded length the same or shortens it.
296+
*/
297+
private void testEvidenceMessageVerification(SRP6GroupParameters group) throws CryptoException
298+
{
299+
byte[] I = "username".getBytes();
300+
byte[] P = "password".getBytes();
301+
byte[] s = new byte[16];
302+
random.nextBytes(s);
303+
304+
SRP6VerifierGenerator gen = new SRP6VerifierGenerator();
305+
gen.init(group, SHA256Digest.newInstance());
306+
BigInteger v = gen.generateVerifier(s, I, P);
307+
308+
SRP6Client client = new SRP6Client();
309+
client.init(group, SHA256Digest.newInstance(), random);
310+
311+
SRP6Server server = new SRP6Server();
312+
server.init(group, v, SHA256Digest.newInstance(), random);
313+
314+
BigInteger A = client.generateClientCredentials(s, I, P);
315+
BigInteger B = server.generateServerCredentials();
316+
317+
client.calculateSecret(B);
318+
server.calculateSecret(A);
319+
320+
BigInteger clientM1 = client.calculateClientEvidenceMessage();
321+
322+
if (!server.verifyClientEvidenceMessage(clientM1))
323+
{
324+
fail("server rejected the correct client evidence message M1");
325+
}
326+
if (server.verifyClientEvidenceMessage(clientM1.add(BigInteger.valueOf(1))))
327+
{
328+
fail("server accepted a tampered client evidence message M1");
329+
}
330+
if (server.verifyClientEvidenceMessage(clientM1.shiftRight(8)))
331+
{
332+
fail("server accepted a truncated client evidence message M1");
333+
}
334+
335+
BigInteger serverM2 = server.calculateServerEvidenceMessage();
336+
337+
if (!client.verifyServerEvidenceMessage(serverM2))
338+
{
339+
fail("client rejected the correct server evidence message M2");
340+
}
341+
if (client.verifyServerEvidenceMessage(serverM2.add(BigInteger.valueOf(1))))
342+
{
343+
fail("client accepted a tampered server evidence message M2");
344+
}
345+
if (client.verifyServerEvidenceMessage(serverM2.shiftRight(8)))
346+
{
347+
fail("client accepted a truncated server evidence message M2");
348+
}
349+
350+
// M1/M2 are digest outputs read as non-negative integers, so a negative value, or one too
351+
// large to be a digest output, is rejected rather than reaching the fixed-width encoding
352+
if (server.verifyClientEvidenceMessage(clientM1.negate()))
353+
{
354+
fail("server accepted a negative client evidence message M1");
355+
}
356+
if (server.verifyClientEvidenceMessage(clientM1.shiftLeft(256)))
357+
{
358+
fail("server accepted an oversized client evidence message M1");
359+
}
360+
if (client.verifyServerEvidenceMessage(serverM2.negate()))
361+
{
362+
fail("client accepted a negative server evidence message M2");
363+
}
364+
if (client.verifyServerEvidenceMessage(serverM2.shiftLeft(256)))
365+
{
366+
fail("client accepted an oversized server evidence message M2");
367+
}
368+
369+
// ...and the correct values still verify afterwards
370+
if (!server.verifyClientEvidenceMessage(clientM1))
371+
{
372+
fail("server rejected the correct client evidence message M1 on retry");
373+
}
374+
if (!client.verifyServerEvidenceMessage(serverM2))
375+
{
376+
fail("client rejected the correct server evidence message M2 on retry");
377+
}
378+
}
379+
291380
private void testClientCatchesBadB(SRP6GroupParameters group)
292381
{
293382
byte[] I = "username".getBytes();

0 commit comments

Comments
 (0)