Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOW-896818 Limited support for encrypted private keys #1671

Merged
merged 3 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import net.snowflake.client.ConditionalIgnoreRule;
import net.snowflake.client.RunningOnGithubActions;
import net.snowflake.client.category.TestCategoryFips;
import net.snowflake.client.core.SecurityUtil;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.crypto.CryptoServicesRegistrar;
import org.bouncycastle.crypto.fips.FipsStatus;
Expand Down Expand Up @@ -161,7 +162,7 @@ public static void setup() throws Exception {
}

// attempts an SSL connection to Google
//connectToGoogle();
// connectToGoogle();
}

@AfterClass
Expand Down Expand Up @@ -205,9 +206,10 @@ public static void teardown() throws Exception {
JAVA_SYSTEM_PROPERTY_SSL_TRUSTSTORE_TYPE,
JAVA_SYSTEM_PROPERTY_SSL_TRUSTSTORE_TYPE_ORIGINAL_VALUE);
}
System.clearProperty(SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM);

// attempts an SSL connection to Google
//connectToGoogle();
// connectToGoogle();
}

@Test
Expand Down Expand Up @@ -319,6 +321,22 @@ public void connectWithFipsAndPut() throws Exception {
}
}

/** Added in > 3.15.1 */
@Test
@ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubActions.class)
public void connectWithFipsKeyPairWithBouncyCastle() throws Exception {
System.setProperty(SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM, "true");
connectWithFipsKeyPair();
}

/** Added in > 3.15.1 */
@Test
@ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubActions.class)
public void testConnectUsingKeyPairWithBouncyCastle() throws Exception {
System.setProperty(SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM, "true");
testConnectUsingKeyPair();
}

private static void connectToGoogle() throws Exception {
URL url = new URL("https://www.google.com/");
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
Expand Down
42 changes: 1 addition & 41 deletions src/main/java/net/snowflake/client/core/SFTrustManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
Expand All @@ -29,8 +28,6 @@
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
Expand Down Expand Up @@ -155,9 +152,6 @@ public class SFTrustManager extends X509ExtendedTrustManager {
private static final ASN1ObjectIdentifier SHA512RSA =
new ASN1ObjectIdentifier("1.2.840.113549.1.1.13").intern();

private static final String DEFAULT_SECURITY_PROVIDER_NAME =
"org.bouncycastle.jce.provider.BouncyCastleProvider";

private static final String ALGORITHM_SHA1_NAME = "SHA-1";
/** Object mapper for JSON encoding and decoding */
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.getObjectMapper();
Expand All @@ -175,10 +169,7 @@ public class SFTrustManager extends X509ExtendedTrustManager {
private static final int DEFAULT_OCSP_RESPONDER_CONNECTION_TIMEOUT = 10000;
/** Default OCSP Cache server host name */
private static final String DEFAULT_OCSP_CACHE_HOST = "http://ocsp.snowflakecomputing.com";
/** provider name */
private static final String BOUNCY_CASTLE_PROVIDER = "BC";
/** provider name for FIPS */
private static final String BOUNCY_CASTLE_FIPS_PROVIDER = "BCFIPS";

/** OCSP response file cache directory */
private static final FileCacheManager fileCacheManager;
/** Tolerable validity date range ratio. */
Expand Down Expand Up @@ -253,37 +244,6 @@ public class SFTrustManager extends X509ExtendedTrustManager {
OCSP_RESPONSE_CODE_TO_STRING.put(OCSPResp.UNAUTHORIZED, "unauthorized");
}

static {
// Add Bouncy Castle to the security provider. This is required to
// verify the signature on OCSP response and attached certificates.
if (Security.getProvider(BOUNCY_CASTLE_PROVIDER) == null
&& Security.getProvider(BOUNCY_CASTLE_FIPS_PROVIDER) == null) {
Security.addProvider(instantiateSecurityProvider());
}
}

private static Provider instantiateSecurityProvider() {
try {
Class klass = Class.forName(DEFAULT_SECURITY_PROVIDER_NAME);
return (Provider) klass.getDeclaredConstructor().newInstance();
} catch (ExceptionInInitializerError
| ClassNotFoundException
| NoSuchMethodException
| InstantiationException
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException
| SecurityException ex) {
String errMsg =
String.format(
"Failed to load %s, err=%s. If you use Snowflake JDBC for FIPS jar, "
+ "import BouncyCastleFipsProvider in the application.",
DEFAULT_SECURITY_PROVIDER_NAME, ex.getMessage());
LOGGER.error(errMsg, true);
throw new RuntimeException(errMsg);
}
}

static {
DATE_FORMAT_UTC.setTimeZone(TimeZone.getTimeZone("UTC"));
}
Expand Down
58 changes: 58 additions & 0 deletions src/main/java/net/snowflake/client/core/SecurityUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package net.snowflake.client.core;

import java.lang.reflect.InvocationTargetException;
import java.security.Provider;
import java.security.Security;
import net.snowflake.client.log.SFLogger;
import net.snowflake.client.log.SFLoggerFactory;

@SnowflakeJdbcInternalApi
public class SecurityUtil {

private static final SFLogger LOGGER = SFLoggerFactory.getLogger(SecurityUtil.class);

/** provider name for FIPS */
public static final String BOUNCY_CASTLE_FIPS_PROVIDER = "BCFIPS";

public static final String BOUNCY_CASTLE_PROVIDER = "BC";
private static final String DEFAULT_SECURITY_PROVIDER_NAME =
"org.bouncycastle.jce.provider.BouncyCastleProvider";

public static final String ENABLE_BOUNCYCASTLE_PROVIDER_JVM =
"net.snowflake.jdbc.enableBouncyCastle";

public static void addBouncyCastleProvider() {
// Add Bouncy Castle to the list of security providers. This is required to
// verify the signature on OCSP response and attached certificates.
// It is also required to decrypt password protected private keys.
// Check to see if the BouncyCastleFipsProvider has already been added.
// If so, then we don't want to add the provider BouncyCastleProvider.
// The addProvider() method won't add the provider if it already exists.
if (Security.getProvider(BOUNCY_CASTLE_FIPS_PROVIDER) == null) {
Security.addProvider(instantiateSecurityProvider());
}
}

private static Provider instantiateSecurityProvider() {

try {
Class klass = Class.forName(DEFAULT_SECURITY_PROVIDER_NAME);
return (Provider) klass.getDeclaredConstructor().newInstance();
} catch (ExceptionInInitializerError
| ClassNotFoundException
| NoSuchMethodException
| InstantiationException
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException
| SecurityException ex) {
String errMsg =
String.format(
"Failed to load %s, err=%s. If you use Snowflake JDBC for FIPS jar, "
+ "import BouncyCastleFipsProvider in the application.",
DEFAULT_SECURITY_PROVIDER_NAME, ex.getMessage());
LOGGER.error(errMsg, true);
throw new RuntimeException(errMsg, ex);
}
}
}
144 changes: 110 additions & 34 deletions src/main/java/net/snowflake/client/core/SessionUtilKeyPair.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Files;
Expand All @@ -37,6 +38,15 @@
import net.snowflake.client.log.SFLogger;
import net.snowflake.client.log.SFLoggerFactory;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;
import org.bouncycastle.util.io.pem.PemReader;

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

private Provider SecurityProvider = null;

private SecretKeyFactory secretKeyFactory = null;

private static final String ISSUER_FMT = "%s.%s.%s";

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

private static final int JWT_DEFAULT_AUTH_TIMEOUT = 10;

private boolean isBouncyCastleProviderEnabled = false;

SessionUtilKeyPair(
PrivateKey privateKey,
String privateKeyFile,
Expand All @@ -75,10 +85,14 @@ class SessionUtilKeyPair {
throws SFException {
this.userName = userName.toUpperCase();
this.accountName = accountName.toUpperCase();

String enableBouncyCastleJvm =
System.getProperty(SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM);
if (enableBouncyCastleJvm != null) {
isBouncyCastleProviderEnabled = enableBouncyCastleJvm.equalsIgnoreCase("true");
}
// check if in FIPS mode
for (Provider p : Security.getProviders()) {
if ("BCFIPS".equals(p.getName())) {
if (SecurityUtil.BOUNCY_CASTLE_FIPS_PROVIDER.equals(p.getName())) {
this.isFipsMode = true;
this.SecurityProvider = p;
break;
Expand Down Expand Up @@ -133,37 +147,31 @@ private SecretKeyFactory getSecretKeyFactory(String algorithm) throws NoSuchAlgo

private PrivateKey extractPrivateKeyFromFile(String privateKeyFile, String privateKeyFilePwd)
throws SFException {
try {
String privateKeyContent = new String(Files.readAllBytes(Paths.get(privateKeyFile)));
if (Strings.isNullOrEmpty(privateKeyFilePwd)) {
// unencrypted private key file
PemReader pr = new PemReader(new StringReader(privateKeyContent));
byte[] decoded = pr.readPemObject().getContent();
pr.close();
PKCS8EncodedKeySpec encodedKeySpec = new PKCS8EncodedKeySpec(decoded);
KeyFactory keyFactory = getKeyFactoryInstance();
return keyFactory.generatePrivate(encodedKeySpec);
} else {
// encrypted private key file
PemReader pr = new PemReader(new StringReader(privateKeyContent));
byte[] decoded = pr.readPemObject().getContent();
pr.close();
EncryptedPrivateKeyInfo pkInfo = new EncryptedPrivateKeyInfo(decoded);
PBEKeySpec keySpec = new PBEKeySpec(privateKeyFilePwd.toCharArray());
SecretKeyFactory pbeKeyFactory = this.getSecretKeyFactory(pkInfo.getAlgName());
PKCS8EncodedKeySpec encodedKeySpec =
pkInfo.getKeySpec(pbeKeyFactory.generateSecret(keySpec));
KeyFactory keyFactory = getKeyFactoryInstance();
return keyFactory.generatePrivate(encodedKeySpec);

if (isBouncyCastleProviderEnabled) {
try {
return extractPrivateKeyWithBouncyCastle(privateKeyFile, privateKeyFilePwd);
} catch (IOException | PKCSException | OperatorCreationException e) {
logger.error("Could not extract private key using Bouncy Castle provider", e);
throw new SFException(e, ErrorCode.INVALID_OR_UNSUPPORTED_PRIVATE_KEY, e.getCause());
}
} else {
try {
return extractPrivateKeyWithJdk(privateKeyFile, privateKeyFilePwd);
} catch (NoSuchAlgorithmException
| InvalidKeySpecException
| IOException
| IllegalArgumentException
| NullPointerException
| InvalidKeyException e) {
logger.error(
"Could not extract private key. Try setting the JVM argument: " + "-D{}" + "=TRUE",
SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM);
throw new SFException(
e,
ErrorCode.INVALID_OR_UNSUPPORTED_PRIVATE_KEY,
privateKeyFile + ": " + e.getMessage());
}
} catch (NoSuchAlgorithmException
| InvalidKeySpecException
| IOException
| IllegalArgumentException
| NullPointerException
| InvalidKeyException e) {
throw new SFException(
e, ErrorCode.INVALID_OR_UNSUPPORTED_PRIVATE_KEY, privateKeyFile + ": " + e.getMessage());
}
}

Expand Down Expand Up @@ -222,4 +230,72 @@ public static int getTimeout() {
}
return jwtAuthTimeout;
}

private PrivateKey extractPrivateKeyWithBouncyCastle(
String privateKeyFile, String privateKeyFilePwd)
throws IOException, PKCSException, OperatorCreationException {
PrivateKeyInfo privateKeyInfo = null;
PEMParser pemParser = new PEMParser(new FileReader(Paths.get(privateKeyFile).toFile()));
Object pemObject = pemParser.readObject();
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
// Handle the case where the private key is encrypted.
PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo =
(PKCS8EncryptedPrivateKeyInfo) pemObject;
InputDecryptorProvider pkcs8Prov =
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(privateKeyFilePwd.toCharArray());
privateKeyInfo = encryptedPrivateKeyInfo.decryptPrivateKeyInfo(pkcs8Prov);
} else if (pemObject instanceof PEMKeyPair) {
// PKCS#1 private key
privateKeyInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
} else if (pemObject instanceof PrivateKeyInfo) {
// Handle the case where the private key is unencrypted.
privateKeyInfo = (PrivateKeyInfo) pemObject;
}
pemParser.close();
JcaPEMKeyConverter converter =
new JcaPEMKeyConverter()
.setProvider(
isFipsMode
? SecurityUtil.BOUNCY_CASTLE_FIPS_PROVIDER
: SecurityUtil.BOUNCY_CASTLE_PROVIDER);
return converter.getPrivateKey(privateKeyInfo);
}

private PrivateKey extractPrivateKeyWithJdk(String privateKeyFile, String privateKeyFilePwd)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
String privateKeyContent = new String(Files.readAllBytes(Paths.get(privateKeyFile)));
if (Strings.isNullOrEmpty(privateKeyFilePwd)) {
// unencrypted private key file
return generatePrivateKey(false, privateKeyContent, privateKeyFilePwd);
} else {
// encrypted private key file
return generatePrivateKey(true, privateKeyContent, privateKeyFilePwd);
}
}

private PrivateKey generatePrivateKey(
boolean isEncrypted, String privateKeyContent, String privateKeyFilePwd)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
if (isEncrypted) {
try (PemReader pr = new PemReader(new StringReader(privateKeyContent))) {
byte[] decoded = pr.readPemObject().getContent();
pr.close();
EncryptedPrivateKeyInfo pkInfo = new EncryptedPrivateKeyInfo(decoded);
PBEKeySpec keySpec = new PBEKeySpec(privateKeyFilePwd.toCharArray());
SecretKeyFactory pbeKeyFactory = this.getSecretKeyFactory(pkInfo.getAlgName());
PKCS8EncodedKeySpec encodedKeySpec =
pkInfo.getKeySpec(pbeKeyFactory.generateSecret(keySpec));
KeyFactory keyFactory = getKeyFactoryInstance();
return keyFactory.generatePrivate(encodedKeySpec);
}
} else {
try (PemReader pr = new PemReader(new StringReader(privateKeyContent))) {
byte[] decoded = pr.readPemObject().getContent();
pr.close();
PKCS8EncodedKeySpec encodedKeySpec = new PKCS8EncodedKeySpec(decoded);
KeyFactory keyFactory = getKeyFactoryInstance();
return keyFactory.generatePrivate(encodedKeySpec);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.sql.SQLFeatureNotSupportedException;
import java.util.List;
import java.util.Properties;
import net.snowflake.client.core.SecurityUtil;
import net.snowflake.common.core.ResourceBundleManager;
import net.snowflake.common.core.SqlState;

Expand Down Expand Up @@ -54,6 +55,8 @@ public class SnowflakeDriver implements Driver {
* Get the manifest properties here.
*/
initializeClientVersionFromManifest();

SecurityUtil.addBouncyCastleProvider();
}

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