porque é que o java não envia o certificado de cliente durante o aperto de mão SSL?
Depois de vários dias de frustração, pesquisa interminável e perguntar a todos ao redor eu descobri que o único problema era que java optou por não enviar o certificado de cliente para o servidor durante o aperto de mão.
especificamente:
- o servidor solicitou um certificado de cliente (CN=RootCA) - ou seja, "dá-me uma certeza que é assinada pela raiz CA"
- Java olhou para o teclado e só encontrou o certificado do meu cliente que é assinado pela" SubCA", que por sua vez é emitido pela"RootCA". Não se deu ao trabalho de olhar para a loja...está bem, acho eu. Infelizmente, quando tentei adicionar o certificado "SubCA" ao teclado, isso não ajudou em nada. Eu verifiquei se os certificados são carregados no teclado. Sim, mas o KeyManager ignora todos os certificados excepto o cliente um.
- Tudo o que precede leva ao fato de que java decide que não tem nenhum certificado que satisfaça a solicitação do servidor e não envia nada...falha no aperto de mão tadaaa: - ([11]}
As minhas perguntas:
-
É possível que tenha adicionado o certificado " SubCA "ao teclado de uma forma que" quebrou a cadeia de certificados " ou algo assim para que o KeyManager só carregue o certificado do cliente e ignore o resto? (Chrome e openssl conseguem descobrir isso assim porque não pode java? - note que o certificado "SubCA" é sempre apresentado separadamente como a Autoridade de confiança, por isso o Chrome aparentemente embala-o correctamente juntamente com o certificado do cliente durante o aperto de mão)
- isto é um "problema de configuração" formal do lado do servidor? O servidor é um terceiro. Eu esperaria que o servidor pedisse um certificado assinado pela Autoridade "Subma", já que foi isso que eles nos forneceram. Eu suspeito que o fato de que isso funciona em Chrome e openssl é porque eles são " menos restritivo "e java apenas vai" de acordo com as regras " e falha.
4 answers
keytool -v -list -keystore store.jks
. Se você só conseguir um certificado por entrada alias, eles não estão juntos.
Teria de importar o seu certificado e a sua cadeia juntos para o nome falso que tem a sua chave privada.
Para descobrir qual é o pseudónimo Keystone a chave privada, use keytool -list -keystore store.jks
(Estou assumindo o tipo de loja JKS aqui). Isto vai dizer - te algo assim:
Your keystore contains 1 entry
myalias, Feb 15, 2012, PrivateKeyEntry,
Certificate fingerprint (MD5): xxxxxxxx
Aqui, o nome falso é myalias
. Se utilizar -v
para além disto, deve ver Alias Name: myalias
.
Se não o tiver já separadamente, exporte o seu certificado de cliente a partir do teclado:
keytool -exportcert -rfc -file clientcert.pem -keystore store.jks -alias myalias
Isto deve dar-te um ficheiro PEM.
Usando um editor de texto (ou cat
), prepare o ficheiro (vamos chamá-lo bundle.pem
) com o certificado do cliente e o intermediário Certificado de AC (e possivelmente o certificado de root CA em si, se você quiser), de modo que o certificado de cliente está no início e seu certificado de Emissor está logo abaixo.
Isto deve parecer como:
-----BEGIN CERTIFICATE-----
MIICajCCAdOgAwIBAgIBAjANBgkqhkiG9w0BAQUFADA7MQswCQYDVQQGEwJVSzEa
....
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICkjCCAfugAwIBAgIJAKm5bDEMxZd7MA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNV
....
-----END CERTIFICATE-----
Agora, importem este pacote de volta para o nome falso onde a vossa chave privada é:
keytool -importcert -keystore store.jks -alias myalias -file bundle.pem
Como uma adição aqui, poderá usar % > OpenSSL S_ Client-connect host. exemplo. com: 443 e ver o 'dump' e verificar se todo o certificado principal é válido para o cliente. Você está procurando por isso na parte inferior da saída. Verificar o código de retorno: 0 (ok)
Se adicionar - showcerts , irá descarregar toda a informação do chaveiro que foi enviado juntamente com o certificado da máquina, que é o que carregou no seu chaveiro.
a razão foi a falta de uma extensão TLS SNI (indicação do nome do servidor) e a seguir é o solucao.
Adicionar uma extensão / parâmetro ao contexto SSL
Após a introdução do SSLContext, adicione o seguinte:
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)
Nota 1: Sslcontextextexception and KeyStoreFactoryException simply extend RuntimeException.
Nota 2: as validações dos certificados estão desactivadas, este exemplo destinava-se apenas à utilização do dev.
Nota 3: a desactivação da verificação do nome da máquina não foi necessária no meu caso, mas eu incluiu-o como uma linha comentada
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());
}
}
Usando o construtor do cliente acima
Nota 1: keystore (.p12) é procurado na pasta" Recursos".
Nota 2: o cabeçalho "máquina" está definido para evitar o erro do servidor "400 - má solicitação".
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;
Docs de Referência
A coisa é que ao usar o certificado cliente assinado por um intermediário você precisa incluir o intermediário em seu trustore para que java possa encontrá-lo. Seja solo ou como um pacote com a ca emissora de raiz.