Error HttpClient 403 al usar un certificado de cliente válido

Estoy tratando de automatizar algunas tareas en un sitio web utilizando Java. Tengo un cliente válido para ese sitio web (funciona cuando inicio sesión con firefox), pero recibo un error 403 cuando bash iniciar sesión utilizando el cliente http. Tenga en cuenta que quiero que mi almacén de confianza confíe en cualquier cosa (sé que no es seguro, pero en este punto no me preocupa eso).

Aquí está mi código:

KeyStore keystore = getKeyStore();//Implemented somewhere else and working ok String password = "changeme"; SSLContext context = SSLContext.getInstance("SSL"); KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmfactory.init(keystore, password.toCharArray()); context.init(kmfactory.getKeyManagers(), new TrustManager[] { new X509TrustManager() { @Override public void checkClientTrusted(java.security.cert.X509Certificate[] arg0, String arg1) throws CertificateException { } @Override public void checkServerTrusted(java.security.cert.X509Certificate[] arg0, String arg1) throws CertificateException { } @Override public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } } }, new SecureRandom()); SSLSocketFactory sf = new SSLSocketFactory(context); Scheme httpsScheme = new Scheme("https", 443, sf); SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(httpsScheme); ClientConnectionManager cm = new SingleClientConnManager(schemeRegistry); HttpClient client = new DefaultHttpClient(cm); HttpGet get = new HttpGet("https://theurl.com"); HttpResponse response = client.execute(get); System.out.println(EntityUtils.toString(response.getEntity(), "UTF-8")); 

La última statement imprime un error 403. ¿Que me estoy perdiendo aqui?

Lo descubrí propio mío. El error 403 que recibía era porque Java SSL no estaba seleccionando mi certificado de cliente.

Depuré el protocolo de enlace SSL y encontré que el servidor solicitó un certificado de cliente emitido por una lista de autoridades y el emisor de mi certificado de cliente no estaba en esa lista. Así que Java SSL simplemente no pudo encontrar un certificado apropiado en mi almacén de claves. Parece que los navegadores web y Java implementan SSL de forma un poco diferente, ya que mi navegador realmente me pregunta qué certificado usar, independientemente de lo que solicite el certificado del servidor en términos de emisores de certificados de clientes.

En este caso, el certificado del servidor es el culpable. Es autofirmado y la lista de emisores que informa como aceptable está incompleta. Y eso no se combina bien con la implementación de Java SSL. Pero el servidor no es mío y no hay nada que pueda hacer al respecto, excepto quejarme del gobierno brasileño (su servidor). Sin más, aquí está mi trabajo:

Primero, usé un TrustManager que confía en cualquier cosa (como hice en mi pregunta):

 public class MyTrustManager implements X509TrustManager { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public X509Certificate[] getAcceptedIssuers() { return null; } } 

Luego implementé un administrador de claves que siempre usa la clave que deseo de un certificado PKCS12 (.pfx):

 public class MyKeyManager extends X509ExtendedKeyManager { KeyStore keystore = null; String password = null; public MyKeyManager(KeyStore keystore, String password) { this.keystore = keystore; this.password = password; } @Override public String chooseClientAlias(String[] arg0, Principal[] arg1, Socket arg2) { return "";//can't be null } @Override public String chooseServerAlias(String arg0, Principal[] arg1, Socket arg2) { return null; } @Override public X509Certificate[] getCertificateChain(String arg0) { try { X509Certificate[] result = new X509Certificate[keystore.getCertificateChain(keystore.aliases().nextElement()).length]; for (int i=0; i < result.length; i++){ result[i] = (X509Certificate) keystore.getCertificateChain(keystore.aliases().nextElement())[i]; } return result ; } catch (Exception e) { } return null; } @Override public String[] getClientAliases(String arg0, Principal[] arg1) { try { return new String[] { keystore.aliases().nextElement() }; } catch (Exception e) { return null; } } @Override public PrivateKey getPrivateKey(String arg0) { try { return ((KeyStore.PrivateKeyEntry) keystore.getEntry(keystore.aliases().nextElement(), new KeyStore.PasswordProtection(password.toCharArray()))).getPrivateKey(); } catch (Exception e) { } return null; } @Override public String[] getServerAliases(String arg0, Principal[] arg1) { return null; } } 

Esto funcionaría si mi pfx también contuviera su certificado de emisor. Pero no lo hace (yay!). Entonces, cuando utilicé el administrador de claves como anteriormente, recibí un error de intercambio de SSL (peer no autenticado). El servidor solo autentica al cliente si el cliente envía una cadena de certificados en la que el servidor confía. Dado que mi certificado (emitido por una agencia brasileña) no contiene su emisor, su cadena de certificados contiene solo a sí misma. Al servidor no le gusta eso y se niega a autenticar al cliente. La solución consiste en crear manualmente la cadena de certificados:

 ... @Override //The order matters, your certificate should be the first one in the chain, its issuer the second, its issuer's issuer the third and so on. public X509Certificate[] getCertificateChain(String arg0) { X509Certificate[] result = new X509Certificate[2]; //The certificate chain contains only one entry in my case result[0] = (X509Certificate) keystore.getCertificateChain(keystore.aliases().nextElement())[0]; //Implement getMyCertificateIssuer() according to your needs. In my case, I read it from a JKS keystore from my database result[1] = getMyCertificateIssuer(); return result; } ... 

Después de eso, fue solo una cuestión de darles un buen uso a mis administradores de confianza y claves personalizadas:

  InputStream keystoreContents = null;//Read it from a file, a byte array or whatever floats your boat KeyStore keystore = KeyStore.getInstance("PKCS12"); keystore.load(keystoreContetns, "changeme".toCharArray()); SSLContext context = SSLContext.getInstance("TLSv1"); context.init(new KeyManager[] { new MyKeyManager(keystore, "changeme") }, new TrustManager[] { new MyTrustManager() }, new SecureRandom()); SSLSocketFactory sf = new SSLSocketFactory(context); Scheme httpsScheme = new Scheme("https", 443, sf); SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(httpsScheme); ClientConnectionManager cm = new SingleClientConnManager(schemeRegistry); HttpClient client = new DefaultHttpClient(cm); HttpPost post = new HttpPost("https://www.someserver.com"); 

En primer lugar, sin embargo, si decide solucionar su problema, no utilice un administrador de confianza que confíe en nada. Desde el punto de vista del cliente, no tiene nada que ver con el certificado de cliente que se va a utilizar, pero abre la conexión a posibles ataques MITM (aunque el atacante MITM aún tendría problemas para hacerse pasar por el cliente legítimo).

Para el aspecto del certificado de cliente, en lugar de usar su propio administrador de claves que reconstruye la cadena cada vez de manera personalizada, puede arreglar su almacén de claves para configurar la entrada del certificado de cliente para que use la cadena completa, como se describe en esta respuesta . Ya que está comenzando con un archivo PFX (PKCS # 12), podría ser mejor convertir su almacén de claves a JKS primero, con keytool -importkeystore , como se describe en esta respuesta .