Java – httpclient 403 error when using valid client certificate
I'm trying to automate some tasks on the website using Java I have a valid client for the website (works when I log in using Firefox), but when I try to log in using the HTTP client, I always receive 403 errors Please note that I want my trust store to trust anything (I know it's not safe, but I'm not worried at this time)
This is my code:
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"));
The last statement prints a 403 error What did I miss here?
Solution
Think of my own 403 error because Java SSL did not select my client certificate
I debugged the SSL handshake and found that the server requires a client certificate issued by the permission list, and the issuer of my client certificate is not in the list Therefore, Java SSL cannot find the appropriate certificate in my keystore It seems that the way web browser and Java implement SSL is slightly different, because my browser will actually ask me which certificate to use, no matter what the server certificate requires in terms of the issuer of the client certificate
In this case, the server certificate is the responsibility It is self - signed and the list of publishers it notifies is incomplete This is not perfect with the Java SSL implementation But the server is not mine. I can do nothing but complain about the Brazilian government (their server) No further expiration, this is my job:
First, I used a trustmanager that trusts everything (as I did in my question):
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; } }
Then I implemented a key manager that always uses the pkcs12 (. PFX) certificate key I want:
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,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; } }
If my PFX also contains its issuer certificate, this will be valid But it didn't (yeah!) Therefore, when I use the above key manager, I receive an SSL handshake error (the peer is not authenticated) If the client sends a certificate chain trusted by the server, the server authenticates the client only Since my certificate (issued by Brazilian institutions) does not include its issuer, its certificate chain only includes itself The server does not like this and refuses to authenticate the client The solution is to manually create a certificate chain:
... @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; } ...
After that, just use my custom key and trust manager:
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",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");