我自己想通了。出现 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 握手错误(对等体未通过身份验证)。仅当客户端发送服务器信任的证书链时,服务器才会对客户端进行身份验证。由于我的证书(由巴西机构颁发)不包含其颁发者,因此其证书链仅包含其自身。服务器不喜欢这样并拒绝对客户端进行身份验证。解决方法是手动创建证书链:
...
@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");