それは自分自身のものであると考えました。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");