Вопрос-ответ

why doesn't java send the client certificate during SSL handshake?

почему java не отправляет клиентский сертификат во время SSL-квитирования?

Я пытаюсь подключиться к безопасному веб-сервису.

Я получал сбой квитирования, хотя мое хранилище ключей и truststore были установлены правильно.

После нескольких дней разочарования, бесконечного поиска в Google и расспросов всех вокруг я выяснил, что единственной проблемой было то, что java решила не отправлять клиентский сертификат на сервер во время квитирования.

В частности:


  1. Сервер запросил клиентский сертификат (CN = RootCA) - т.е. "дайте мне сертификат, подписанный корневым центром сертификации"

  2. Java заглянула в хранилище ключей и нашла только мой клиентский сертификат, подписанный "SubCA", который, в свою очередь, выдается "RootCA". Она не потрудилась заглянуть в truststore...думаю, все в порядке

  3. К сожалению, когда я попытался добавить сертификат "SubCA" в хранилище ключей, это вообще не помогло. Я проверил, загружаются ли сертификаты в хранилище ключей. Они отправляют, но KeyManager игнорирует все сертификаты, кроме клиентского.

  4. Все вышесказанное приводит к тому, что java решает, что у нее нет сертификатов, удовлетворяющих запросу сервера, и ничего не отправляет...tadaaa сбой квитирования :-(

Мои вопросы:


  1. Возможно ли, что я добавил сертификат "SubCA" в хранилище ключей способом, который "разорвал цепочку сертификатов" или что-то в этом роде, так что KeyManager загружает только клиентский сертификат и игнорирует остальные? (Chrome и openssl справляются с этим, так почему java не может? - обратите внимание, что сертификат "SubCA" всегда представлен отдельно как доверенный орган, поэтому Chrome, по-видимому, корректно упаковывает его вместе с клиентским сертификатом во время квитирования)

  2. Это формальная "проблема с конфигурацией" на стороне сервера? Сервер является сторонним. Я бы ожидал, что сервер запросит сертификат, подписанный полномочным органом "SubCA", поскольку именно это они нам предоставили. Я подозреваю, что тот факт, что это работает в Chrome и openssl, объясняется тем, что они "менее ограничительны", а java просто работает "по инструкции" и дает сбой.

Мне удалось найти грязный обходной путь для этого, но я не очень доволен этим, поэтому я буду рад, если кто-нибудь сможет прояснить это для меня.

Переведено автоматически
Ответ 1

Возможно, вы импортировали промежуточный сертификат CA в хранилище ключей, не связывая его с записью, в которой у вас есть клиентский сертификат и его закрытый ключ. Вы должны быть в состоянии увидеть это с помощью keytool -v -list -keystore store.jks. Если вы получаете только один сертификат для каждой записи псевдонима, они не объединены.

Вам нужно будет импортировать ваш сертификат и его цепочку вместе в псевдоним хранилища ключей, который содержит ваш закрытый ключ.

Чтобы узнать, какой псевдоним хранилища ключей содержит закрытый ключ, используйте keytool -list -keystore store.jks (Я предполагаю, что здесь используется тип хранилища JKS). Это подскажет вам что-то вроде этого:

Your keystore contains 1 entry

myalias, Feb 15, 2012, PrivateKeyEntry,
Certificate fingerprint (MD5): xxxxxxxx

Здесь используется псевдоним myalias. Если вы используете -v в дополнение к этому, вы должны увидеть Alias Name: myalias.

Если у вас его еще нет отдельно, экспортируйте клиентский сертификат из хранилища ключей:

keytool -exportcert -rfc -file clientcert.pem -keystore store.jks -alias myalias

Это должно дать вам PEM-файл.

Используя текстовый редактор (или cat), подготовьте файл (назовем его bundle.pem) с этим клиентским сертификатом и промежуточным сертификатом CA (и, возможно, с самим сертификатом корневого CA, если хотите), чтобы клиентский сертификат был в начале, а сертификат его эмитента - чуть ниже.

Это должно выглядеть следующим образом:

-----BEGIN CERTIFICATE-----
MIICajCCAdOgAwIBAgIBAjANBgkqhkiG9w0BAQUFADA7MQswCQYDVQQGEwJVSzEa
....
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICkjCCAfugAwIBAgIJAKm5bDEMxZd7MA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNV
....
-----END CERTIFICATE-----

Теперь импортируйте этот пакет обратно вместе с псевдонимом, в котором находится ваш закрытый ключ:

keytool -importcert -keystore store.jks -alias myalias -file bundle.pem
Ответ 2

As an add here, you can use %> openssl s_client -connect host.example.com:443 and see the dump and check that all the main cert is valid against the client. You are looking for this at the bottom of the output.
Verify return code: 0 (ok)

If you add -showcerts it will dump all the info of the keychain that was sent along with the host certificate, which is what you loaded into your keychain.

Ответ 3

Most of the solutions I've seen revolve around using the keytool and none of them matched my case.

Here is a very brief description: I've got a PKCS12 (.p12) which works fine in Postman with disabled certificate verification, however programmatically I always ended up getting server error "400 Bad Request" / "No required SSL certificate was sent".

The reason was a missing TLS extension SNI (Server Name Indication) and following is the solution.


Adding an extension/parameter to SSL Context

After SSLContext init, add the following:

SSLSocketFactory factory = sslContext.getSocketFactory();
try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
SSLParameters sslParameters = socket.getSSLParameters();
sslParameters.setServerNames(Collections.singletonList(new SNIHostName(hostName)));
socket.setSSLParameters(sslParameters);
socket.startHandshake();
}

Full HTTP Client class for this case (NOT FOR PRODUCTION)

Note 1: SSLContextException and KeyStoreFactoryException simply extend RuntimeException.

Note 2: Certificate validations are disabled, this example was intended for dev use only.

Note 3: Disabling hostname verification was not required in my case, but I included it as a commented line

import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;

import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Objects;

public class SecureClientBuilder {

private String host;
private int port;
private boolean keyStoreProvided;
private String keyStorePath;
private String keyStorePassword;

public SecureClientBuilder withSocket(String host, int port) {
this.host = host;
this.port = port;
return this;
}

public SecureClientBuilder withKeystore(String keyStorePath, String keyStorePassword) {
this.keyStoreProvided = true;
this.keyStorePath = keyStorePath;
this.keyStorePassword = keyStorePassword;
return this;
}

public CloseableHttpClient build() {
SSLContext sslContext = keyStoreProvided
? getContextWithCertificate()
: SSLContexts.createDefault();

SSLConnectionSocketFactory sslSocketFactory =
new SSLConnectionSocketFactory(sslContext);

return HttpClients.custom()
.setSSLSocketFactory(sslSocketFactory)
//.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
.build();
}

private SSLContext getContextWithCertificate() {
try {
// Generate TLS context with specified KeyStore and
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(getKeyManagerFactory().getKeyManagers(), new TrustManager[]{getTrustManager()}, new SecureRandom());

SSLSocketFactory factory = sslContext.getSocketFactory();
try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
SSLParameters sslParameters = socket.getSSLParameters();
sslParameters.setServerNames(Collections.singletonList(new SNIHostName(host)));
socket.setSSLParameters(sslParameters);
socket.startHandshake();
}

return sslContext;
} catch (NoSuchAlgorithmException | KeyManagementException | IOException e) {
throw new SSLContextException("Could not create an SSL context with specified keystore.\nError: " + e.getMessage());
}
}

private KeyManagerFactory getKeyManagerFactory() {
try (FileInputStream fileInputStream = getResourceFile(keyStorePath)) {
// Read specified keystore
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(fileInputStream, keyStorePassword.toCharArray());

// Init keystore manager
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
return keyManagerFactory;
} catch (NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | IOException | KeyStoreException e) {
throw new KeyStoreFactoryException("Could not read the specified keystore.\nError: " + e.getMessage());
}
}

// Bypasses error: "unable to find valid certification path to requested target"
private TrustManager getTrustManager() {
return new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] arg0, String arg1) {
}

@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] arg0, String arg1) {
}

@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
}

private FileInputStream getResourceFile(String keyStorePath) throws FileNotFoundException {
URL resourcePath = getClass().getClassLoader().getResource(keyStorePath);
return new FileInputStream(resourcePath.getFile());
}

}

Using the client builder above

Note 1: keystore (.p12) is looked for in "resources" folder.

Note 2: Header "Host" is set to avoid server error "400 - Bad Request".

String hostname = "myHost";
int port = 443;
String keyStoreFile = "keystore.p12";
String keyStorePass = "somepassword";
String endpoint = String.format("https://%s:%d/endpoint", host, port);

CloseableHttpClient apacheClient = new SecureClientBuilder()
.withSocket(hostname, port)
.withKeystore(keyStoreFile, keyStorePass)
.build();

HttpGet get = new HttpGet(endpoint);
get.setHeader("Host", hostname + ":" + port);
CloseableHttpResponse httpResponse = apacheClient.execute(get);

assert httpResponse.getStatusLine().getStatusCode() == 200;

Reference docs

https://docs.oracle.com/en/java/javase/11/security/java-secure-socket-extension-jsse-reference-guide.html

Ответ 4

The thing is when using client cert signed by an intermediate one you need to include the intermediate one in your trustore so java can find it. Either solo or as a bundle with the root issuing ca.

java