Skip to content

Commit d3d00d9

Browse files
committed
Locate, open and close the BCJSSE default key/trust stores inside AccessController.doPrivileged, so the FilePermission for the JRE's jssecacerts/cacerts is required of the provider rather than of every protection domain on the call stack.
1 parent 9c9ad88 commit d3d00d9

4 files changed

Lines changed: 84 additions & 57 deletions

File tree

docs/releasenotes.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ <h3>2.1.2 Defects Fixed</h3>
4545
<li>HQC (org.bouncycastle.pqc.crypto.hqc) leaked secret-derived data through two timing/cache side channels. Its GF(2^8) arithmetic (GF) was implemented with log/exp/inv/sqr lookup tables indexed by field elements, so which cache line was touched revealed the operand - the same weakness as an AES T-table implementation. Those tables are reached with secret inputs on both sides of the KEM: ReedSolomon.encode multiplies by coefficients derived from the secret message during encapsulation, and ReedSolomon.decode and FastFourierTransform operate on syndromes, error-locator coefficients and root sets derived from the secret error vector during decapsulation. Separately, the fixed-weight support sampler broke out of its duplicate-detection scan as soon as a collision was found, and stored each accepted position at the secret running index; it samples the secret key supports x and y during key generation and re-expands y from the long-term secret key seed on every decapsulation, where the same seed produces the same draw sequence every time, so any timing variation there is a fixed and repeatedly measurable function of the private key. GF is now table-free and branch-free (carryless multiply with a masked reduction, squaring by bit-spreading, inversion as a^254), and the sampler is branch-free within a batch of candidates: every candidate is examined even once enough positions have been accepted, the duplicate scan visits every slot with no early exit, and an accepted position is written by masking across all slots rather than by indexing with the running count. The sampler's output and its byte consumption from the SHAKE stream are unchanged, so generated keys, ciphertexts and shared secrets are identical and the known-answer vectors still hold; the number of candidate batches it squeezes remains data-dependent, which is inherent to the specified sampler and cannot be changed without diverging from its output. The sibling sampler used for the encapsulation error vectors was already branch-free and is unaffected. HQC is exposed through the lightweight API and the BCPQC provider; the BC provider registers only its key-info converters. Reported by the Robusta team.</li>
4646
<li>NTRU reduced secret values with the % operator in three helpers whose reference implementations are deliberately division-free, so the reduction was carried out by an integer division whose latency depends on the secret operand. Polynomial.modQ(x, q) was x % q with a variable divisor, which no compiler can strength-reduce to a multiply the way it can a constant one, so it emitted a real division on every call - including on the decapsulation path, through rqToS3 and trinaryZqToZ3, where the dividend derives from the private key. It is now x &amp; (q - 1), matching the reference MODQ macro; that is exact because q is always a power of two (NTRUParameterSet.q() returns 1 &lt;&lt; logQ) and every call site passes a dividend already masked to 16 bits. Polynomial.mod3 (both the short and byte overloads) and NTRUSampling.mod3 used % 3 on the secret key polynomials f and g during key generation, on the message polynomials r and m during encapsulation, and on coefficients recovered during decapsulation; they now use the reference implementation's division-free fold-and-select, folding by 255, then 15, then 3 before a single masked conditional subtraction. All four sites produce byte-identical results to the previous code across their entire input domain, so keys, ciphertexts, shared secrets and the known-answer vectors are unchanged. Reported by the Robusta team.</li>
4747
<li>JcePBMac1CalculatorBuilder.build() took the PBKDF2 iterationCount out of an RFC 9579 PBMAC1Params directly, unlike every sibling PBE/PBMAC1 path in the tree (the legacy-PBE branch of JcePKCS12MacCalculatorBuilderProvider, the BC-lightweight BcPKCS12PBMAC1CalculatorBuilder/PKCS12PBEUtils path, PKCS12PBMAC1KeyStoreSpi, and PKCS12Util.calculatePBMAC1), all of which already bound the count before deriving a key. Since PKCS12PfxPdu.isMacValid dispatches an id-PBMAC1 MacData to this builder, and the MAC key must be derived before the MAC can be checked, a PKCS#12 file with an attacker-chosen iteration count (up to 2^31-1) pinned a CPU core running PBKDF2-HMAC for as long as the count demanded before the (unauthenticated) file was ever rejected - a pre-authentication CPU denial of service from a file a few hundred bytes long. The same gap was reachable via JcePBMac1CalculatorProviderBuilder for a PBMAC1-protected CMP PBMProtectedPKIMessage. The iteration count is now bounded by the existing org.bouncycastle.pbe.max_iteration_count property (Properties.PBE_MAX_ITERATION_COUNT, default 10,000,000), throwing OperatorCreationException when exceeded, matching the cap already applied on every sibling path.</li>
48+
<li>Locating the JRE's default trust store in BCJSSE was not done in a privileged block, so under a security manager the java.io.FilePermission for it was required of every protection domain on the call stack rather than of the provider alone. The read of the file was already privileged, but the java.io.File.exists() probes that find it - javax.net.ssl.trustStore, then ${java.home}/lib/security/jssecacerts, then cacerts - were not. In a container that grants the BC jars their own permissions while sandboxing application code, SSLContext.getDefault() (or SSLContext.getInstance("Default", "BCJSSE")) therefore failed with "Default SSL algorithm not found in JRE", caused by a KeyManagementException reporting access denied ("java.io.FilePermission" "...jssecacerts" "read"); wrapping the call in the application's own doPrivileged did not help, since the denied domain is on the stack either way, and because DefaultSSLContextSpi caches the outcome in a static holder the failure was permanent for the life of the JVM. The default key store path (javax.net.ssl.keyStore) had the same gap in ProvKeyManagerFactorySpi and additionally opened and closed its file unprivileged. Locating, opening and closing the default key and trust stores now all run inside AccessController.doPrivileged, so only the provider's own protection domain is consulted; behaviour without a security manager, and where the whole stack is trusted, is unchanged.</li>
4849
</ul>
4950

5051
<h3>2.1.3 Additional Features and Functionality</h3>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package org.bouncycastle.jsse.provider;
2+
3+
import java.io.BufferedInputStream;
4+
import java.io.File;
5+
import java.io.FileInputStream;
6+
import java.io.InputStream;
7+
import java.security.AccessController;
8+
import java.security.PrivilegedAction;
9+
import java.security.PrivilegedActionException;
10+
import java.security.PrivilegedExceptionAction;
11+
12+
/**
13+
* Privileged file system access used when locating and reading the default key/trust stores.
14+
* <p>
15+
* Finding the JRE's default stores is work the provider does on its own behalf rather than on
16+
* behalf of the caller, so each operation runs inside an
17+
* {@link AccessController#doPrivileged(PrivilegedAction)} block. Without it, every protection
18+
* domain on the call stack needs its own {@code java.io.FilePermission} for the JRE's
19+
* {@code jssecacerts} / {@code cacerts} files before something as ordinary as
20+
* {@code SSLContext.getDefault()} will work under a security manager.
21+
*/
22+
class FileUtils
23+
{
24+
static boolean exists(final String path)
25+
{
26+
return AccessController.doPrivileged(new PrivilegedAction<Boolean>()
27+
{
28+
public Boolean run()
29+
{
30+
return Boolean.valueOf(new File(path).exists());
31+
}
32+
}).booleanValue();
33+
}
34+
35+
static InputStream openBufferedInputStream(final String path)
36+
throws Exception
37+
{
38+
try
39+
{
40+
return AccessController.doPrivileged(new PrivilegedExceptionAction<InputStream>()
41+
{
42+
public InputStream run()
43+
throws Exception
44+
{
45+
return new BufferedInputStream(new FileInputStream(path));
46+
}
47+
});
48+
}
49+
catch (PrivilegedActionException e)
50+
{
51+
throw e.getException();
52+
}
53+
}
54+
55+
static void closeInputStream(final InputStream input)
56+
throws Exception
57+
{
58+
try
59+
{
60+
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>()
61+
{
62+
public Void run()
63+
throws Exception
64+
{
65+
input.close();
66+
return null;
67+
}
68+
});
69+
}
70+
catch (PrivilegedActionException e)
71+
{
72+
throw e.getException();
73+
}
74+
}
75+
}

tls/src/main/java/org/bouncycastle/jsse/provider/ProvKeyManagerFactorySpi.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package org.bouncycastle.jsse.provider;
22

3-
import java.io.BufferedInputStream;
4-
import java.io.File;
5-
import java.io.FileInputStream;
63
import java.io.InputStream;
74
import java.security.InvalidAlgorithmParameterException;
85
import java.security.KeyStore;
@@ -41,7 +38,7 @@ static KeyStoreConfig getDefaultKeyStore() throws Exception
4138
}
4239
else if (null != ksPathProp)
4340
{
44-
if (new File(ksPathProp).exists())
41+
if (FileUtils.exists(ksPathProp))
4542
{
4643
ksPath = ksPathProp;
4744
}
@@ -65,7 +62,7 @@ else if (null != ksPathProp)
6562
else
6663
{
6764
LOG.config("Initializing default key store from path: " + ksPath);
68-
ksInput = new BufferedInputStream(new FileInputStream(ksPath));
65+
ksInput = FileUtils.openBufferedInputStream(ksPath);
6966
}
7067

7168
try
@@ -83,7 +80,7 @@ else if (null != ksPathProp)
8380
{
8481
if (null != ksInput)
8582
{
86-
ksInput.close();
83+
FileUtils.closeInputStream(ksInput);
8784
}
8885
}
8986

tls/src/main/java/org/bouncycastle/jsse/provider/ProvTrustManagerFactorySpi.java

Lines changed: 5 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
package org.bouncycastle.jsse.provider;
22

3-
import java.io.BufferedInputStream;
43
import java.io.File;
5-
import java.io.FileInputStream;
64
import java.io.InputStream;
7-
import java.security.AccessController;
85
import java.security.InvalidAlgorithmParameterException;
96
import java.security.KeyStore;
107
import java.security.KeyStoreException;
118
import java.security.NoSuchProviderException;
12-
import java.security.PrivilegedActionException;
13-
import java.security.PrivilegedExceptionAction;
149
import java.security.cert.CertPathParameters;
1510
import java.security.cert.Certificate;
1611
import java.security.cert.PKIXParameters;
@@ -55,7 +50,7 @@ static KeyStore getDefaultTrustStore() throws Exception
5550
}
5651
else if (null != tsPathProp)
5752
{
58-
if (new File(tsPathProp).exists())
53+
if (FileUtils.exists(tsPathProp))
5954
{
6055
tsPath = tsPathProp;
6156
}
@@ -66,7 +61,7 @@ else if (null != tsPathProp)
6661
if (null != javaHome)
6762
{
6863
String jsseCacertsPath = javaHome + "/lib/security/jssecacerts".replace("/", File.separator);
69-
if (new File(jsseCacertsPath).exists())
64+
if (FileUtils.exists(jsseCacertsPath))
7065
{
7166
if (defaultCacertsToJKS)
7267
{
@@ -77,7 +72,7 @@ else if (null != tsPathProp)
7772
else
7873
{
7974
String cacertsPath = javaHome + "/lib/security/cacerts".replace("/", File.separator);
80-
if (new File(cacertsPath).exists())
75+
if (FileUtils.exists(cacertsPath))
8176
{
8277
if (defaultCacertsToJKS)
8378
{
@@ -107,7 +102,7 @@ else if (null != tsPathProp)
107102
else
108103
{
109104
LOG.config("Initializing default trust store from path: " + tsPath);
110-
tsInput = openTrustStoreInput(tsPath);
105+
tsInput = FileUtils.openBufferedInputStream(tsPath);
111106
}
112107

113108
try
@@ -125,7 +120,7 @@ else if (null != tsPathProp)
125120
{
126121
if (null != tsInput)
127122
{
128-
closeTrustStoreInput(tsInput);
123+
FileUtils.closeInputStream(tsInput);
129124
}
130125
}
131126

@@ -230,47 +225,6 @@ private static void collectTrustAnchor(Set<TrustAnchor> trustAnchors, Certificat
230225
}
231226
}
232227

233-
private static InputStream openTrustStoreInput(final String tsPath)
234-
throws Exception
235-
{
236-
try
237-
{
238-
return AccessController.doPrivileged(new PrivilegedExceptionAction<InputStream>()
239-
{
240-
public InputStream run()
241-
throws Exception
242-
{
243-
return new BufferedInputStream(new FileInputStream(tsPath));
244-
}
245-
});
246-
}
247-
catch (PrivilegedActionException e)
248-
{
249-
throw e.getException();
250-
}
251-
}
252-
253-
private static void closeTrustStoreInput(final InputStream tsInput)
254-
throws Exception
255-
{
256-
try
257-
{
258-
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>()
259-
{
260-
public Void run()
261-
throws Exception
262-
{
263-
tsInput.close();
264-
return null;
265-
}
266-
});
267-
}
268-
catch (PrivilegedActionException e)
269-
{
270-
throw e.getException();
271-
}
272-
}
273-
274228
private static KeyStore createTrustStore(String defaultType)
275229
throws NoSuchProviderException, KeyStoreException
276230
{

0 commit comments

Comments
 (0)