使用有效客户端证书时,HttpClient出现403错误

4
我将尝试使用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错误。我在这里错过了什么?
2个回答

7
我自己解决了问题。我遇到的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");

1
我想知道如何调试SSL握手过程以找出问题所在? - Albert Cheng
@AlbertCheng 我启用了SSL调试以查看来回发送的内容。除此之外,还涉及大量的试错。 - Andre
那真是救了我一命,伙计。但是嘿,我预料到会有问题,毕竟这是巴西。干杯,老兄。 - Diego Urenia

1
首先,无论您选择如何解决问题,请不要使用信任任何内容的信任管理器。从客户端的角度来看,它与将要使用的客户端证书无关,但它确实打开了潜在的中间人攻击连接(尽管中间人攻击者仍然会有问题模拟合法客户端)。
对于客户端证书方面,您可以将您的密钥管理器修复为每次以定制方式重新构建链路,而是将您的密钥库设置为使用完整链路的客户端证书条目,如在此答案中所述。由于您从PFX文件(PKCS#12)开始,因此最好先将您的密钥库转换为JKS,使用keytool -importkeystore,如在此答案中所述。

就信任管理器而言,我在原型设计和缩短问题/答案的长度方面使用了一个“杂乱”的管理器。但我不会在生产中使用它。客户端证书方面对我的应用程序有点特殊(你可能不知道)。我希望用户能够自己管理证书并将其pfx文件上传到服务器。我将这些证书保存在我的数据库中,用户可以在需要连接政府时选择要使用的证书。感谢您的建议! - Andre
为什么不将证书链存储在PFX文件中呢?(顺便说一下,关于信任管理器,将用于测试的特定证书导入到测试机器的信任存储中,这样可以使问题更简洁,并进行更真实的测试。) - Bruno
PFX文件是由证书颁发机构直接提供给我的,我不想对其进行任何更改。 - Andre
在这种情况下,如果您控制要发送到的服务器,可以在那里添加中间证书。 - Bruno
我无法控制服务器。 - Andre

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接