Skip to content

Commit 5ff9e9a

Browse files
SNOW-896818 Limited support for encrypted private keys (#1671)
1 parent af90e33 commit 5ff9e9a

File tree

6 files changed

+315
-83
lines changed

6 files changed

+315
-83
lines changed

FIPS/src/test/java/net/snowflake/client/jdbc/ConnectionFipsIT.java

+20-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import net.snowflake.client.ConditionalIgnoreRule;
2424
import net.snowflake.client.RunningOnGithubActions;
2525
import net.snowflake.client.category.TestCategoryFips;
26+
import net.snowflake.client.core.SecurityUtil;
2627
import org.apache.commons.codec.binary.Base64;
2728
import org.bouncycastle.crypto.CryptoServicesRegistrar;
2829
import org.bouncycastle.crypto.fips.FipsStatus;
@@ -161,7 +162,7 @@ public static void setup() throws Exception {
161162
}
162163

163164
// attempts an SSL connection to Google
164-
//connectToGoogle();
165+
// connectToGoogle();
165166
}
166167

167168
@AfterClass
@@ -205,9 +206,10 @@ public static void teardown() throws Exception {
205206
JAVA_SYSTEM_PROPERTY_SSL_TRUSTSTORE_TYPE,
206207
JAVA_SYSTEM_PROPERTY_SSL_TRUSTSTORE_TYPE_ORIGINAL_VALUE);
207208
}
209+
System.clearProperty(SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM);
208210

209211
// attempts an SSL connection to Google
210-
//connectToGoogle();
212+
// connectToGoogle();
211213
}
212214

213215
@Test
@@ -319,6 +321,22 @@ public void connectWithFipsAndPut() throws Exception {
319321
}
320322
}
321323

324+
/** Added in > 3.15.1 */
325+
@Test
326+
@ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubActions.class)
327+
public void connectWithFipsKeyPairWithBouncyCastle() throws Exception {
328+
System.setProperty(SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM, "true");
329+
connectWithFipsKeyPair();
330+
}
331+
332+
/** Added in > 3.15.1 */
333+
@Test
334+
@ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubActions.class)
335+
public void testConnectUsingKeyPairWithBouncyCastle() throws Exception {
336+
System.setProperty(SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM, "true");
337+
testConnectUsingKeyPair();
338+
}
339+
322340
private static void connectToGoogle() throws Exception {
323341
URL url = new URL("https://www.google.com/");
324342
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();

src/main/java/net/snowflake/client/core/SFTrustManager.java

+1-41
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.io.File;
2020
import java.io.IOException;
2121
import java.io.OutputStream;
22-
import java.lang.reflect.InvocationTargetException;
2322
import java.math.BigInteger;
2423
import java.net.URI;
2524
import java.net.URISyntaxException;
@@ -29,8 +28,6 @@
2928
import java.security.KeyStoreException;
3029
import java.security.MessageDigest;
3130
import java.security.NoSuchAlgorithmException;
32-
import java.security.Provider;
33-
import java.security.Security;
3431
import java.security.Signature;
3532
import java.security.SignatureException;
3633
import java.security.cert.CertificateEncodingException;
@@ -155,9 +152,6 @@ public class SFTrustManager extends X509ExtendedTrustManager {
155152
private static final ASN1ObjectIdentifier SHA512RSA =
156153
new ASN1ObjectIdentifier("1.2.840.113549.1.1.13").intern();
157154

158-
private static final String DEFAULT_SECURITY_PROVIDER_NAME =
159-
"org.bouncycastle.jce.provider.BouncyCastleProvider";
160-
161155
private static final String ALGORITHM_SHA1_NAME = "SHA-1";
162156
/** Object mapper for JSON encoding and decoding */
163157
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.getObjectMapper();
@@ -175,10 +169,7 @@ public class SFTrustManager extends X509ExtendedTrustManager {
175169
private static final int DEFAULT_OCSP_RESPONDER_CONNECTION_TIMEOUT = 10000;
176170
/** Default OCSP Cache server host name */
177171
private static final String DEFAULT_OCSP_CACHE_HOST = "http://ocsp.snowflakecomputing.com";
178-
/** provider name */
179-
private static final String BOUNCY_CASTLE_PROVIDER = "BC";
180-
/** provider name for FIPS */
181-
private static final String BOUNCY_CASTLE_FIPS_PROVIDER = "BCFIPS";
172+
182173
/** OCSP response file cache directory */
183174
private static final FileCacheManager fileCacheManager;
184175
/** Tolerable validity date range ratio. */
@@ -253,37 +244,6 @@ public class SFTrustManager extends X509ExtendedTrustManager {
253244
OCSP_RESPONSE_CODE_TO_STRING.put(OCSPResp.UNAUTHORIZED, "unauthorized");
254245
}
255246

256-
static {
257-
// Add Bouncy Castle to the security provider. This is required to
258-
// verify the signature on OCSP response and attached certificates.
259-
if (Security.getProvider(BOUNCY_CASTLE_PROVIDER) == null
260-
&& Security.getProvider(BOUNCY_CASTLE_FIPS_PROVIDER) == null) {
261-
Security.addProvider(instantiateSecurityProvider());
262-
}
263-
}
264-
265-
private static Provider instantiateSecurityProvider() {
266-
try {
267-
Class klass = Class.forName(DEFAULT_SECURITY_PROVIDER_NAME);
268-
return (Provider) klass.getDeclaredConstructor().newInstance();
269-
} catch (ExceptionInInitializerError
270-
| ClassNotFoundException
271-
| NoSuchMethodException
272-
| InstantiationException
273-
| IllegalAccessException
274-
| IllegalArgumentException
275-
| InvocationTargetException
276-
| SecurityException ex) {
277-
String errMsg =
278-
String.format(
279-
"Failed to load %s, err=%s. If you use Snowflake JDBC for FIPS jar, "
280-
+ "import BouncyCastleFipsProvider in the application.",
281-
DEFAULT_SECURITY_PROVIDER_NAME, ex.getMessage());
282-
LOGGER.error(errMsg, true);
283-
throw new RuntimeException(errMsg);
284-
}
285-
}
286-
287247
static {
288248
DATE_FORMAT_UTC.setTimeZone(TimeZone.getTimeZone("UTC"));
289249
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package net.snowflake.client.core;
2+
3+
import java.lang.reflect.InvocationTargetException;
4+
import java.security.Provider;
5+
import java.security.Security;
6+
import net.snowflake.client.log.SFLogger;
7+
import net.snowflake.client.log.SFLoggerFactory;
8+
9+
@SnowflakeJdbcInternalApi
10+
public class SecurityUtil {
11+
12+
private static final SFLogger LOGGER = SFLoggerFactory.getLogger(SecurityUtil.class);
13+
14+
/** provider name for FIPS */
15+
public static final String BOUNCY_CASTLE_FIPS_PROVIDER = "BCFIPS";
16+
17+
public static final String BOUNCY_CASTLE_PROVIDER = "BC";
18+
private static final String DEFAULT_SECURITY_PROVIDER_NAME =
19+
"org.bouncycastle.jce.provider.BouncyCastleProvider";
20+
21+
public static final String ENABLE_BOUNCYCASTLE_PROVIDER_JVM =
22+
"net.snowflake.jdbc.enableBouncyCastle";
23+
24+
public static void addBouncyCastleProvider() {
25+
// Add Bouncy Castle to the list of security providers. This is required to
26+
// verify the signature on OCSP response and attached certificates.
27+
// It is also required to decrypt password protected private keys.
28+
// Check to see if the BouncyCastleFipsProvider has already been added.
29+
// If so, then we don't want to add the provider BouncyCastleProvider.
30+
// The addProvider() method won't add the provider if it already exists.
31+
if (Security.getProvider(BOUNCY_CASTLE_FIPS_PROVIDER) == null) {
32+
Security.addProvider(instantiateSecurityProvider());
33+
}
34+
}
35+
36+
private static Provider instantiateSecurityProvider() {
37+
38+
try {
39+
Class klass = Class.forName(DEFAULT_SECURITY_PROVIDER_NAME);
40+
return (Provider) klass.getDeclaredConstructor().newInstance();
41+
} catch (ExceptionInInitializerError
42+
| ClassNotFoundException
43+
| NoSuchMethodException
44+
| InstantiationException
45+
| IllegalAccessException
46+
| IllegalArgumentException
47+
| InvocationTargetException
48+
| SecurityException ex) {
49+
String errMsg =
50+
String.format(
51+
"Failed to load %s, err=%s. If you use Snowflake JDBC for FIPS jar, "
52+
+ "import BouncyCastleFipsProvider in the application.",
53+
DEFAULT_SECURITY_PROVIDER_NAME, ex.getMessage());
54+
LOGGER.error(errMsg, true);
55+
throw new RuntimeException(errMsg, ex);
56+
}
57+
}
58+
}

src/main/java/net/snowflake/client/core/SessionUtilKeyPair.java

+110-34
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.nimbusds.jose.crypto.RSASSASigner;
1414
import com.nimbusds.jwt.JWTClaimsSet;
1515
import com.nimbusds.jwt.SignedJWT;
16+
import java.io.FileReader;
1617
import java.io.IOException;
1718
import java.io.StringReader;
1819
import java.nio.file.Files;
@@ -37,6 +38,15 @@
3738
import net.snowflake.client.log.SFLogger;
3839
import net.snowflake.client.log.SFLoggerFactory;
3940
import org.apache.commons.codec.binary.Base64;
41+
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
42+
import org.bouncycastle.openssl.PEMKeyPair;
43+
import org.bouncycastle.openssl.PEMParser;
44+
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
45+
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
46+
import org.bouncycastle.operator.InputDecryptorProvider;
47+
import org.bouncycastle.operator.OperatorCreationException;
48+
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
49+
import org.bouncycastle.pkcs.PKCSException;
4050
import org.bouncycastle.util.io.pem.PemReader;
4151

4252
/** Class used to compute jwt token for key pair authentication Created by hyu on 1/16/18. */
@@ -58,14 +68,14 @@ class SessionUtilKeyPair {
5868

5969
private Provider SecurityProvider = null;
6070

61-
private SecretKeyFactory secretKeyFactory = null;
62-
6371
private static final String ISSUER_FMT = "%s.%s.%s";
6472

6573
private static final String SUBJECT_FMT = "%s.%s";
6674

6775
private static final int JWT_DEFAULT_AUTH_TIMEOUT = 10;
6876

77+
private boolean isBouncyCastleProviderEnabled = false;
78+
6979
SessionUtilKeyPair(
7080
PrivateKey privateKey,
7181
String privateKeyFile,
@@ -75,10 +85,14 @@ class SessionUtilKeyPair {
7585
throws SFException {
7686
this.userName = userName.toUpperCase();
7787
this.accountName = accountName.toUpperCase();
78-
88+
String enableBouncyCastleJvm =
89+
System.getProperty(SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM);
90+
if (enableBouncyCastleJvm != null) {
91+
isBouncyCastleProviderEnabled = enableBouncyCastleJvm.equalsIgnoreCase("true");
92+
}
7993
// check if in FIPS mode
8094
for (Provider p : Security.getProviders()) {
81-
if ("BCFIPS".equals(p.getName())) {
95+
if (SecurityUtil.BOUNCY_CASTLE_FIPS_PROVIDER.equals(p.getName())) {
8296
this.isFipsMode = true;
8397
this.SecurityProvider = p;
8498
break;
@@ -133,37 +147,31 @@ private SecretKeyFactory getSecretKeyFactory(String algorithm) throws NoSuchAlgo
133147

134148
private PrivateKey extractPrivateKeyFromFile(String privateKeyFile, String privateKeyFilePwd)
135149
throws SFException {
136-
try {
137-
String privateKeyContent = new String(Files.readAllBytes(Paths.get(privateKeyFile)));
138-
if (Strings.isNullOrEmpty(privateKeyFilePwd)) {
139-
// unencrypted private key file
140-
PemReader pr = new PemReader(new StringReader(privateKeyContent));
141-
byte[] decoded = pr.readPemObject().getContent();
142-
pr.close();
143-
PKCS8EncodedKeySpec encodedKeySpec = new PKCS8EncodedKeySpec(decoded);
144-
KeyFactory keyFactory = getKeyFactoryInstance();
145-
return keyFactory.generatePrivate(encodedKeySpec);
146-
} else {
147-
// encrypted private key file
148-
PemReader pr = new PemReader(new StringReader(privateKeyContent));
149-
byte[] decoded = pr.readPemObject().getContent();
150-
pr.close();
151-
EncryptedPrivateKeyInfo pkInfo = new EncryptedPrivateKeyInfo(decoded);
152-
PBEKeySpec keySpec = new PBEKeySpec(privateKeyFilePwd.toCharArray());
153-
SecretKeyFactory pbeKeyFactory = this.getSecretKeyFactory(pkInfo.getAlgName());
154-
PKCS8EncodedKeySpec encodedKeySpec =
155-
pkInfo.getKeySpec(pbeKeyFactory.generateSecret(keySpec));
156-
KeyFactory keyFactory = getKeyFactoryInstance();
157-
return keyFactory.generatePrivate(encodedKeySpec);
150+
151+
if (isBouncyCastleProviderEnabled) {
152+
try {
153+
return extractPrivateKeyWithBouncyCastle(privateKeyFile, privateKeyFilePwd);
154+
} catch (IOException | PKCSException | OperatorCreationException e) {
155+
logger.error("Could not extract private key using Bouncy Castle provider", e);
156+
throw new SFException(e, ErrorCode.INVALID_OR_UNSUPPORTED_PRIVATE_KEY, e.getCause());
157+
}
158+
} else {
159+
try {
160+
return extractPrivateKeyWithJdk(privateKeyFile, privateKeyFilePwd);
161+
} catch (NoSuchAlgorithmException
162+
| InvalidKeySpecException
163+
| IOException
164+
| IllegalArgumentException
165+
| NullPointerException
166+
| InvalidKeyException e) {
167+
logger.error(
168+
"Could not extract private key. Try setting the JVM argument: " + "-D{}" + "=TRUE",
169+
SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM);
170+
throw new SFException(
171+
e,
172+
ErrorCode.INVALID_OR_UNSUPPORTED_PRIVATE_KEY,
173+
privateKeyFile + ": " + e.getMessage());
158174
}
159-
} catch (NoSuchAlgorithmException
160-
| InvalidKeySpecException
161-
| IOException
162-
| IllegalArgumentException
163-
| NullPointerException
164-
| InvalidKeyException e) {
165-
throw new SFException(
166-
e, ErrorCode.INVALID_OR_UNSUPPORTED_PRIVATE_KEY, privateKeyFile + ": " + e.getMessage());
167175
}
168176
}
169177

@@ -222,4 +230,72 @@ public static int getTimeout() {
222230
}
223231
return jwtAuthTimeout;
224232
}
233+
234+
private PrivateKey extractPrivateKeyWithBouncyCastle(
235+
String privateKeyFile, String privateKeyFilePwd)
236+
throws IOException, PKCSException, OperatorCreationException {
237+
PrivateKeyInfo privateKeyInfo = null;
238+
PEMParser pemParser = new PEMParser(new FileReader(Paths.get(privateKeyFile).toFile()));
239+
Object pemObject = pemParser.readObject();
240+
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
241+
// Handle the case where the private key is encrypted.
242+
PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo =
243+
(PKCS8EncryptedPrivateKeyInfo) pemObject;
244+
InputDecryptorProvider pkcs8Prov =
245+
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(privateKeyFilePwd.toCharArray());
246+
privateKeyInfo = encryptedPrivateKeyInfo.decryptPrivateKeyInfo(pkcs8Prov);
247+
} else if (pemObject instanceof PEMKeyPair) {
248+
// PKCS#1 private key
249+
privateKeyInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
250+
} else if (pemObject instanceof PrivateKeyInfo) {
251+
// Handle the case where the private key is unencrypted.
252+
privateKeyInfo = (PrivateKeyInfo) pemObject;
253+
}
254+
pemParser.close();
255+
JcaPEMKeyConverter converter =
256+
new JcaPEMKeyConverter()
257+
.setProvider(
258+
isFipsMode
259+
? SecurityUtil.BOUNCY_CASTLE_FIPS_PROVIDER
260+
: SecurityUtil.BOUNCY_CASTLE_PROVIDER);
261+
return converter.getPrivateKey(privateKeyInfo);
262+
}
263+
264+
private PrivateKey extractPrivateKeyWithJdk(String privateKeyFile, String privateKeyFilePwd)
265+
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
266+
String privateKeyContent = new String(Files.readAllBytes(Paths.get(privateKeyFile)));
267+
if (Strings.isNullOrEmpty(privateKeyFilePwd)) {
268+
// unencrypted private key file
269+
return generatePrivateKey(false, privateKeyContent, privateKeyFilePwd);
270+
} else {
271+
// encrypted private key file
272+
return generatePrivateKey(true, privateKeyContent, privateKeyFilePwd);
273+
}
274+
}
275+
276+
private PrivateKey generatePrivateKey(
277+
boolean isEncrypted, String privateKeyContent, String privateKeyFilePwd)
278+
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
279+
if (isEncrypted) {
280+
try (PemReader pr = new PemReader(new StringReader(privateKeyContent))) {
281+
byte[] decoded = pr.readPemObject().getContent();
282+
pr.close();
283+
EncryptedPrivateKeyInfo pkInfo = new EncryptedPrivateKeyInfo(decoded);
284+
PBEKeySpec keySpec = new PBEKeySpec(privateKeyFilePwd.toCharArray());
285+
SecretKeyFactory pbeKeyFactory = this.getSecretKeyFactory(pkInfo.getAlgName());
286+
PKCS8EncodedKeySpec encodedKeySpec =
287+
pkInfo.getKeySpec(pbeKeyFactory.generateSecret(keySpec));
288+
KeyFactory keyFactory = getKeyFactoryInstance();
289+
return keyFactory.generatePrivate(encodedKeySpec);
290+
}
291+
} else {
292+
try (PemReader pr = new PemReader(new StringReader(privateKeyContent))) {
293+
byte[] decoded = pr.readPemObject().getContent();
294+
pr.close();
295+
PKCS8EncodedKeySpec encodedKeySpec = new PKCS8EncodedKeySpec(decoded);
296+
KeyFactory keyFactory = getKeyFactoryInstance();
297+
return keyFactory.generatePrivate(encodedKeySpec);
298+
}
299+
}
300+
}
225301
}

src/main/java/net/snowflake/client/jdbc/SnowflakeDriver.java

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.sql.SQLFeatureNotSupportedException;
1515
import java.util.List;
1616
import java.util.Properties;
17+
import net.snowflake.client.core.SecurityUtil;
1718
import net.snowflake.common.core.ResourceBundleManager;
1819
import net.snowflake.common.core.SqlState;
1920

@@ -54,6 +55,8 @@ public class SnowflakeDriver implements Driver {
5455
* Get the manifest properties here.
5556
*/
5657
initializeClientVersionFromManifest();
58+
59+
SecurityUtil.addBouncyCastleProvider();
5760
}
5861

5962
/** try to initialize Arrow support if fails, JDBC is going to use the legacy format */

0 commit comments

Comments
 (0)