WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Draft
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
95 changes: 95 additions & 0 deletions src/main/java/com/mongodb/jdbc/utils/X509Authentication.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
import com.mongodb.MongoException;
import com.mongodb.jdbc.logging.MongoLogger;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.security.*;
import java.security.cert.Certificate;
import java.security.UnrecoverableKeyException;
import java.util.logging.Level;
import javax.net.ssl.*;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
Expand Down Expand Up @@ -132,6 +134,54 @@ public void configureX509Authentication(
}
}

/**
* Configures X.509 authentication for MongoDB using a JKS keystore containing the private key and
* certificate.
*
* @param settingsBuilder The MongoDB client settings builder to apply the SSL configuration.
* Must not be null.
* @param keystorePath The path to the JKS keystore file containing the private key and certificate.
* Must not be null.
* @param keystorePassword The password for the keystore. Can be null if keystore is not password protected.
* @param certificateAlias The alias of the certificate to use from the keystore. Must not be null.
* @throws Exception If there is an error during configuration, keystore loading, or certificate extraction.
* @throws NullPointerException if settingsBuilder, keystorePath, or certificateAlias are null.
*/
public void configureX509AuthenticationFromKeystore(
com.mongodb.MongoClientSettings.Builder settingsBuilder,
String keystorePath,
char[] keystorePassword,
String certificateAlias)
throws Exception {

if (settingsBuilder == null) {
throw new NullPointerException("settingsBuilder cannot be null");
}
if (keystorePath == null || keystorePath.trim().isEmpty()) {
throw new NullPointerException("keystorePath cannot be null or empty");
}
if (certificateAlias == null || certificateAlias.trim().isEmpty()) {
throw new NullPointerException("certificateAlias cannot be null or empty");
}

logger.log(Level.FINE, "Using JKS keystore for X509 authentication: " + keystorePath);
logger.log(Level.FINE, "Certificate alias: " + certificateAlias);

try {
SSLContext sslContext =
createSSLContextFromKeystore(keystorePath, keystorePassword, certificateAlias);

settingsBuilder.applyToSslSettings(
sslSettings -> {
sslSettings.enabled(true);
sslSettings.context(sslContext);
});
} catch (Exception e) {
logger.log(Level.SEVERE, "SSL setup failed: " + e.getMessage());
throw e;
}
}

/**
* Formats a PEM string to handle escaped newlines and ensures correct header placement. Adds
* required newlines for compatibility with Bouncy Castle PEMParser.
Expand Down Expand Up @@ -381,6 +431,51 @@ private SSLContext createSSLContextFromKeyAndCert(PrivateKey privateKey, Certifi
return sslContext;
}

private SSLContext createSSLContextFromKeystore(
String keystorePath, char[] keystorePassword, String certificateAlias)
throws Exception {
KeyStore keystore = KeyStore.getInstance("JKS");
try (FileInputStream keystoreStream = new FileInputStream(keystorePath)) {
keystore.load(keystoreStream, keystorePassword);
logger.log(Level.FINE, "Successfully loaded JKS keystore from: " + keystorePath);
} catch (IOException e) {
throw new MongoException("Failed to read keystore file: " + e.getMessage(), e);
} catch (Exception e) {
throw new MongoException("Failed to load keystore: " + e.getMessage(), e);
}

Certificate cert = keystore.getCertificate(certificateAlias);
if (cert == null) {
throw new MongoException(
"Certificate with alias '" + certificateAlias + "' not found in keystore");
}
logger.log(Level.FINE, "Found certificate with alias: " + certificateAlias);

PrivateKey privateKey;
try {
Key key = keystore.getKey(certificateAlias, keystorePassword);
if (key == null) {
throw new MongoException(
"Private key with alias '" + certificateAlias + "' not found in keystore");
}
if (!(key instanceof PrivateKey)) {
throw new MongoException("Key with alias '" + certificateAlias + "' is not a private key");
}
privateKey = (PrivateKey) key;
logger.log(
Level.FINE, "Successfully extracted private key with alias: " + certificateAlias);
} catch (UnrecoverableKeyException e) {
throw new MongoException(
"Failed to extract private key with alias '"
+ certificateAlias
+ "': "
+ e.getMessage(),
e);
}

return createSSLContextFromKeyAndCert(privateKey, cert);
}

private PemAuthenticationInput parsePemAuthenticationInput(String input) {
try {
BsonDocument doc = BsonDocument.parse(input);
Expand Down
201 changes: 201 additions & 0 deletions src/test/java/com/mongodb/jdbc/utils/PemToKeystoreConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
* Copyright 2025-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mongodb.jdbc.utils;

import com.mongodb.jdbc.logging.MongoLogger;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;

/**
* Utility class to convert PEM files to JKS keystore format for testing purposes.
* This class reads unencrypted PEM files from the test resources directory and
* creates a JKS keystore with certificates using filenames as aliases.
*/
public class PemToKeystoreConverter {
private static final Logger LOGGER = Logger.getLogger("PemToKeystoreConverter");
private static final String TEST_PEM_DIR = "X509AuthenticationTest";
private static final String KEYSTORE_PASSWORD = "testpass";
private static final String KEYSTORE_FILENAME = "test-certificates.jks";

static {
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setFormatter(new SimpleFormatter());
consoleHandler.setLevel(Level.ALL);
LOGGER.addHandler(consoleHandler);
LOGGER.setLevel(Level.ALL);
LOGGER.setUseParentHandlers(false);
}

private static final MongoLogger MONGO_LOGGER = new MongoLogger(LOGGER, 1);

/**
* Main method to convert PEM files to JKS keystore.
* Finds all *unencrypted.pem files in the test resources directory,
* extracts certificates and private keys, and creates a JKS keystore.
*/
public static void main(String[] args) throws Exception {
PemToKeystoreConverter converter = new PemToKeystoreConverter();
converter.convertPemFilesToKeystore();
}

/**
* Converts all unencrypted PEM files to a JKS keystore.
*/
public void convertPemFilesToKeystore() throws Exception {
LOGGER.info("Starting PEM to JKS keystore conversion...");

java.security.Security.addProvider(new BouncyCastleProvider());

KeyStore keystore = KeyStore.getInstance("JKS");
keystore.load(null, KEYSTORE_PASSWORD.toCharArray());

ClassLoader classLoader = getClass().getClassLoader();
File testDir = new File(classLoader.getResource(TEST_PEM_DIR).getFile());

File[] pemFiles = testDir.listFiles((dir, name) -> name.endsWith("unencrypted.pem"));

if (pemFiles == null || pemFiles.length == 0) {
LOGGER.warning("No unencrypted PEM files found in " + testDir.getPath());
return;
}

LOGGER.info("Found " + pemFiles.length + " unencrypted PEM files");

for (File pemFile : pemFiles) {
processPemFile(pemFile, keystore);
}

String keystorePath = testDir.getParent() + File.separator + KEYSTORE_FILENAME;
try (FileOutputStream fos = new FileOutputStream(keystorePath)) {
keystore.store(fos, KEYSTORE_PASSWORD.toCharArray());
LOGGER.info("Keystore saved to: " + keystorePath);
}

verifyKeystore(keystorePath);
}

/**
* Processes a single PEM file and adds its certificate and private key to the keystore.
*/
private void processPemFile(File pemFile, KeyStore keystore) throws Exception {
String filename = pemFile.getName();
String alias = filename.substring(0, filename.lastIndexOf(".pem"));

LOGGER.info("Processing file: " + filename + " with alias: " + alias);

Certificate certificate = null;
PrivateKey privateKey = null;

try (FileReader fileReader = new FileReader(pemFile);
PEMParser pemParser = new PEMParser(fileReader)) {

Object pemObject;
while ((pemObject = pemParser.readObject()) != null) {
if (pemObject instanceof X509CertificateHolder) {
X509CertificateHolder certHolder = (X509CertificateHolder) pemObject;
JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
converter.setProvider("BC");
certificate = converter.getCertificate(certHolder);
LOGGER.fine("Found X.509 certificate in " + filename);
} else if (pemObject instanceof PEMKeyPair) {
PEMKeyPair keyPair = (PEMKeyPair) pemObject;
JcaPEMKeyConverter keyConverter = new JcaPEMKeyConverter();
keyConverter.setProvider("BC");
privateKey = keyConverter.getPrivateKey(keyPair.getPrivateKeyInfo());
LOGGER.fine("Found PKCS#1 private key in " + filename);
} else if (pemObject instanceof PrivateKeyInfo) {
PrivateKeyInfo keyInfo = (PrivateKeyInfo) pemObject;
JcaPEMKeyConverter keyConverter = new JcaPEMKeyConverter();
keyConverter.setProvider("BC");
privateKey = keyConverter.getPrivateKey(keyInfo);
LOGGER.fine("Found PKCS#8 private key in " + filename);
}
}
}

if (certificate == null) {
throw new Exception("No certificate found in " + filename);
}
if (privateKey == null) {
throw new Exception("No private key found in " + filename);
}

Certificate[] certChain = {certificate};
keystore.setKeyEntry(alias, privateKey, KEYSTORE_PASSWORD.toCharArray(), certChain);

LOGGER.info("Added certificate and private key for alias: " + alias);
}

/**
* Verifies the created keystore by loading it and checking its contents.
*/
private void verifyKeystore(String keystorePath) throws Exception {
LOGGER.info("Verifying keystore contents...");

KeyStore verifyKeystore = KeyStore.getInstance("JKS");
try (FileInputStream fis = new FileInputStream(keystorePath)) {
verifyKeystore.load(fis, KEYSTORE_PASSWORD.toCharArray());
}

java.util.Enumeration<String> aliases = verifyKeystore.aliases();
int count = 0;
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
count++;

Certificate cert = verifyKeystore.getCertificate(alias);
PrivateKey key = (PrivateKey) verifyKeystore.getKey(alias, KEYSTORE_PASSWORD.toCharArray());

LOGGER.info("Verified alias: " + alias +
" - Certificate: " + (cert != null ? "OK" : "MISSING") +
" - Private Key: " + (key != null ? "OK" : "MISSING"));
}

LOGGER.info("Keystore verification complete. Total aliases: " + count);
}

/**
* Gets the keystore password used for testing.
*/
public static String getKeystorePassword() {
return KEYSTORE_PASSWORD;
}

/**
* Gets the keystore filename.
*/
public static String getKeystoreFilename() {
return KEYSTORE_FILENAME;
}
}