通用图片加载器 | SSL握手异常:握手失败

3

我有一个ListView,其中包含一些内容(TextView、ImageView等)的项。我正在使用Nostra的UIL来加载项目中的图像,但其中一些图像无法加载。当我调用Log.v(String.valueOf(failReason.getCause());时,会得到以下结果:

11-16 23:52:20.447: V/javax.net.ssl.SSLHandshakeException: Handshake failed(17467): failz
11-16 23:52:20.657: V/NativeCrypto(17467): SSL handshake aborted: ssl=0x15fd758: Failure in SSL library, usually a protocol error
11-16 23:52:20.657: V/NativeCrypto(17467): error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:762 0x4c2ed485:0x00000000)
11-16 23:52:21.207: V/NativeCrypto(17467): SSL handshake aborted: ssl=0x1562468: Failure in SSL library, usually a protocol error
11-16 23:52:21.207: V/NativeCrypto(17467): error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:762 0x4c2ed485:0x00000000)

你不知道为什么会出现这个问题,或者该如何解决它吗?

以下是一个无法加载的示例图片:

http://bigparty.cz/photos/headlinefoto/13.jpg

(我可以附上一个带有完整错误的日志 - 这个错误是 UIL 自动放到日志中的)

3个回答

9
如果我没记错的话,您需要创建一个证书,对其进行签名并将其包含在您的应用程序中。或者更改服务器配置(此处有更多信息)。
否则,您可以在应用程序中信任每个握手。虽然这不是最佳方法,但在实施期间非常有用。
将此类包含在您的项目中。
public class SSLCertificateHandler {

protected static final String TAG = "NukeSSLCerts";

/**
 * Enables https connections
 */
public static void nuke() {
    try {
        TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
            public X509Certificate[] getAcceptedIssuers() {
                X509Certificate[] myTrustedAnchors = new X509Certificate[0];
                return myTrustedAnchors;
            }

            @Override
            public void checkClientTrusted(X509Certificate[] certs, String authType) {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] certs, String authType) {
            }
        } };

        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, trustAllCerts, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String arg0, SSLSession arg1) {
                return true;
            }
        });
    } catch (Exception e) {
    }
}

}

扩展你的应用程序,并在onCreate中调用'nuke'函数

public class YOURApplication extends Application {

@Override
public void onCreate() {
    super.onCreate();

    //...

    // trust all SSL -> HTTPS connection
    SSLCertificateHandler.nuke();
}

我在Stack Overflow上找到了这段代码,但目前找不到链接...

1
我知道在开发过程中这很有用。但是一旦应用程序上线,绝对不要考虑接受所有证书。这将完全打败使用HTTPS的目的,与其这样,还不如使用HTTP。更多信息请参见此处(但请忽略此答案)。 - Krishnabhadra
可以在Java中包含一个条件,以便仅在开发构建时调用SSLCertificateHandler.nuke() - 请参见https://dev59.com/A2Ag5IYBdhLWcg3wOoxQ#23844716。 - ban-geoengineering

2
如longilong所述,信任所有证书并不是最好的方法。实际上,在生产环境中这是非常糟糕的方法,因为它基本上使SSL失效,并使您容易受到中间人攻击的攻击。
问题归结为POODLE SSL3漏洞,该漏洞已导致一些(大多数?)服务提供商禁用其SSLv3支持。这对我们Android开发人员来说是个坏消息,因为HttpConnection在Build.VERSION.SDK_INT> = 9 && Build.VERSION.SDK_INT <= 20中默认使用SSLv3。
Android问题跟踪器中有一个问题,其中提供了更多信息和解决方案,包括提供一个自定义的SSLFactory,拒绝将SSLv3设置为唯一的回退协议,以下是该问题的代码。我对此解决方案不承担任何责任,但它是我们在Lollipop之前的所有版本中使用的方法。
/**
 * An {@link javax.net.ssl.SSLSocket} that doesn't allow {@code SSLv3} only connections
 * <p>fixes https://github.com/koush/ion/issues/386</p>
 */
private static class NoSSLv3SSLSocket extends DelegateSSLSocket {

private NoSSLv3SSLSocket(SSLSocket delegate) {
    super(delegate);

    String canonicalName = delegate.getClass().getCanonicalName();
    if (!canonicalName.equals("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl")){
        // try replicate the code from HttpConnection.setupSecureSocket()
        try {
            Method msetUseSessionTickets = delegate.getClass().getMethod("setUseSessionTickets", boolean.class);
            if (null != msetUseSessionTickets) {
                msetUseSessionTickets.invoke(delegate, true);
            }
        } catch (NoSuchMethodException ignored) {
        } catch (InvocationTargetException ignored) {
        } catch (IllegalAccessException ignored) {
        }
    }
}

@Override
public void setEnabledProtocols(String[] protocols) {
    if (protocols != null && protocols.length == 1 && "SSLv3".equals(protocols[0])) {
        // no way jose
        // see issue https://code.google.com/p/android/issues/detail?id=78187
        List<String> enabledProtocols = new ArrayList<String>(Arrays.asList(delegate.getEnabledProtocols()));
        if (enabledProtocols.size() > 1) {
            enabledProtocols.remove("SSLv3");
        } else {
            LogManager.getLogger().w("SSL stuck with protocol available for " + String.valueOf(enabledProtocols));
        }
        protocols = enabledProtocols.toArray(new String[enabledProtocols.size()]);
    }
    super.setEnabledProtocols(protocols);
}
}


/**
 * {@link javax.net.ssl.SSLSocketFactory} that doesn't allow {@code SSLv3} only connections
 */
private static class NoSSLv3Factory extends SSLSocketFactory {
private final SSLSocketFactory delegate;

private NoSSLv3Factory() {
    this.delegate = HttpsURLConnection.getDefaultSSLSocketFactory();
}

@Override
public String[] getDefaultCipherSuites() {
    return delegate.getDefaultCipherSuites();
}

@Override
public String[] getSupportedCipherSuites() {
    return delegate.getSupportedCipherSuites();
}

private static Socket makeSocketSafe(Socket socket) {
    if (socket instanceof SSLSocket) {
        socket = new NoSSLv3SSLSocket((SSLSocket) socket);
    }
    return socket;
}

@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
    return makeSocketSafe(delegate.createSocket(s, host, port, autoClose));
}

@Override
public Socket createSocket(String host, int port) throws IOException {
    return makeSocketSafe(delegate.createSocket(host, port));
}

@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
    return makeSocketSafe(delegate.createSocket(host, port, localHost, localPort));
}

@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
    return makeSocketSafe(delegate.createSocket(host, port));
}

@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
    return makeSocketSafe(delegate.createSocket(address, port, localAddress, localPort));
}
}

static {
HttpsURLConnection.setDefaultSSLSocketFactory(new NoSSLv3Factory());
}

这对我帮助很大。我希望它也能帮助其他人。谢谢。 - marson

2
可能是您的服务器端缺少中间CA证书。
尝试阅读以下内容:
大多数公共CA不会直接签署服务器证书。相反,它们使用其主要CA证书(称为根CA)来签署中间CA。他们这样做是为了将根CA存储在离线状态,以减少被攻击的风险。但是,像Android这样的操作系统通常仅直接信任根CA,这就在服务器证书(由中间CA签署)和证书验证器之间留下了一个短暂的信任空白。为了解决这个问题,服务器在SSL握手期间不仅向客户端发送其证书,而且还发送从服务器CA通过任何必要的中间件到达受信任的根CA的证书链。
为了看到这在实践中是什么样子,以下是通过openssl s_client命令查看的mail.google.com证书链:

i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority

这表明服务器发送了一个由Thawte SGC CA颁发的用于mail.google.com的证书,该证书是一个中间CA,以及一个由Verisign CA颁发的用于Thawte SGC CA的第二个证书,Verisign CA是Android信任的主要CA。

然而,配置服务器不包括必要的中间CA并不罕见。例如,以下是一个可以导致Android浏览器错误和Android应用程序异常的服务器:

$ openssl s_client -connect egov.uscis.gov:443

证书链 0 s:/ C = US / ST = District Of Columbia / L = Washington / O =美国国土安全部/ OU =美国公民和移民服务局/ OU =在www.verisign.com/rpa(c)05 / CN = egov.uscis.gov上使用条款

i:/C=US/O=VeriSign,Inc./OU=VeriSign Trust Network/OU=https://www.verisign.com/rpa使用条款(c)10 / CN=VeriSign Class 3 International Server CA-G3

有趣的是,在大多数桌面浏览器中访问此服务器不会像完全未知的CA或自签名的服务器证书一样导致错误。这是因为大多数桌面浏览器随着时间的推移缓存受信任的中间CA。一旦浏览器从一个站点访问并学习了关于中间CA的信息,下次就不需要在证书链中包含中间CA。

一些网站故意为用于提供资源的二级Web服务器执行此操作。例如,他们可能通过具有完整证书链的服务器提供其主HTML页面,但是对于提供图像、CSS或JavaScript等资源的服务器,不包括CA,可能是为了节省带宽。不幸的是,有时这些服务器可能正在提供您尝试从Android应用程序调用的Web服务,而Android应用程序并不如此宽容。

解决此问题有两种方法:

配置服务器以在服务器链中包含中间CA。大多数CA都提供了如何在所有常见的Web服务器上执行此操作的文档。如果您需要该站点在至少通过Android 4.2的默认Android浏览器上工作,则只有这种方法可行。 或者,将中间CA视为任何其他未知CA,并创建一个TrustManager直接信任它,就像前两个部分所做的那样。

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