使用有效的客户端证书时HttpClient 403错误

我正在尝试使用Java在网站上自动执行某些任务。 我有一个有效的网站客户端(当我使用firefox登录时工作),但当我尝试使用http客户端登录时,我一直收到403错误。 请注意,我希望我的信任商店信任任何东西(我知道它不安全,但此时我并不担心)。

这是我的代码:

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")); 

最后一个语句打印出403错误。 我在这里想念的是什么?

想出来我自己的。 403错误是因为Java SSL没有选择我的客户端证书。

我调试了SSL握手,发现服务器要求提供由权限列表颁发的客户端证书,而我的客户端证书的颁发者不在该列表中。 因此,Java SSL无法在我的密钥库中找到合适的证书。 看起来Web浏览器和Java实现SSL有点不同,因为我的浏览器实际上询问我使用哪个证书,无论服务器证书在客户端证书的颁发者方面要求什么。

在这种情况下,服务器证书是责任。 它是自签名的,并且它通知的发布者列表是不完整的。 这与Java SSL实现并不完美。 但服务器不是我的,除了抱怨巴西政府(他们的服务器)之外,我无能为力。 没有进一步的到期,这是我的工作:

首先,我使用了一个信任任何东西的TrustManager(就像我在我的问题中所做的那样):

 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; } } 

然后我实现了一个密钥管理器,它始终使用我想要的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; } } 

如果我的pfx还包含其颁发者证书,这有效。 但它没有(耶!)。 因此,当我使用上面的密钥管理器时,我收到了SSL握手错误(对等体未经过身份validation)。 如果客户端发送服务器信任的证书链,则服务器仅对客户端进行身份validation。 由于我的证书(由巴西机构发行)不包含其颁发者,因此其证书链仅包含其自身。 服务器不喜欢这样,并拒绝validation客户端。 解决方法是手动创建证书链:

 ... @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; } ... 

在那之后,只需要使用我的自定义密钥和信任管理器:

  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"); 

首先,无论您选择解决问题,都不要使用信任任何东西的信任管理器。 从客户端的角度来看,它与将要使用的客户端证书无关,但它确实打开了与潜在MITM攻击的连接(尽管MITM攻击者仍会在模拟合法客户端时遇到问题)。

对于客户端证书方面,您可以修复密钥库以设置客户端证书条目以使用完整链,而不是使用您自己的密钥管理器(每次以定制方式重新构建链),如本答案中所述 。 由于您开始使用PFX文件(PKCS#12),因此最好使用keytool -importkeystore将密钥库首先转换为JKS,如本答案中所述 。