Android 上的自签名 SSL 接受

Posted

技术标签:

【中文标题】Android 上的自签名 SSL 接受【英文标题】:Self-signed SSL acceptance on Android 【发布时间】:2010-11-16 01:43:47 【问题描述】:

如何在 android 上接受 Java 中的自签名证书?

代码示例将是完美的。

我在 Internet 上到处查看,虽然有些人声称找到了解决方案,但它要么不起作用,要么没有示例代码来支持它。

【问题讨论】:

【参考方案1】:

我在 exchangeIt 中有这个功能,它通过 WebDav 连接到 Microsoft Exchange。下面是一些创建 HttpClient 的代码,它将通过 SSL 连接到自签名证书:

SchemeRegistry schemeRegistry = new SchemeRegistry();
// http scheme
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
// https scheme
schemeRegistry.register(new Scheme("https", new EasySSLSocketFactory(), 443));

HttpParams params = new BasicHttpParams();
params.setParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 30);
params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, new ConnPerRouteBean(30));
params.setParameter(HttpProtocolParams.USE_EXPECT_CONTINUE, false);
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);

ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);

EasySSLSocketFactory 是here,EasyX509TrustManager 是here。

exchangeIt 的代码是开源的,托管在 googlecode here,如果您有任何问题。我不再积极处理它了,但代码应该可以工作。

请注意,自 Android 2.2 以来,该过程发生了一些变化,因此请检查 this 以使上述代码正常工作。

【讨论】:

我无法让它工作。我在 2.2 上不断收到IOException: SSL handshake failure: I/O error during system call, Broken pipe。你碰巧知道解决这个问题的方法吗? 我不喜欢信任任何自签名证书。有没有办法添加证书颁发机构以防止中间人攻击? 三个params.setParameter() 是必需的吗?第三个需要使用 HTTP 1.1 我们可以将此程序用于正常托管和 facebook 应用程序 就像@NicolasMarchildon 所说,这很危险,因为它会将您的应用程序暴露给中间人攻击。 Here 是您关于 Cert pinning 的一个很好的解释。【参考方案2】:

正如 EJP 正确评论的那样,“读者应该注意,这种技术根本不安全。除非至少对一个对等方进行身份验证,否则 SSL 是不安全的。请参阅 RFC 2246。”

话虽如此,这是另一种方式,没有任何额外的类:

import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.X509TrustManager;

private void trustEveryone() 
    try 
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier()
                public boolean verify(String hostname, SSLSession session) 
                    return true;
                );
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, new X509TrustManager[]new X509TrustManager()
            public void checkClientTrusted(X509Certificate[] chain,
                    String authType) throws CertificateException 
            public void checkServerTrusted(X509Certificate[] chain,
                    String authType) throws CertificateException 
            public X509Certificate[] getAcceptedIssuers() 
                return new X509Certificate[0];
            , new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(
                context.getSocketFactory());
     catch (Exception e)  // should never happen
        e.printStackTrace();
    

【讨论】:

你会在哪里调用这个方法? 在打开 https 连接之前,您可以在任何地方调用它。任何使用 URL.openConnection / HttpsURLConnection 的连接都会受到影响。 我找不到任何解决方案,除了这个。这是一个可怕的黑客,但谢谢。 读者应该注意,这种技术根本不安全。 SSL 是不安全的,除非至少有一个对等方经过身份验证。请参阅 RFC 2246。 没有安全性的黑客是不好的 :)【参考方案3】:

我昨天在将我们公司的 RESTful API 迁移到 HTTPS 时遇到了这个问题,但使用的是自签名 SSL 证书。

我到处寻找,但我发现所有“正确”标记的答案都包括禁用证书验证,显然覆盖了 SSL 的所有意义。

我终于找到了解决办法:

    创建本地密钥库

    要使您的应用能够验证您的自签名证书,您需要以 Android 可以信任您的端点的方式提供带有证书的自定义密钥库。

此类自定义密钥库的格式是来自BouncyCastle 的“BKS”,因此您需要可以下载here 的 1.46 版本的 BouncyCastleProvider。

您还需要您的自签名证书,我假设它的名称为self_cert.pem

现在创建密钥库的命令是:

<!-- language: lang-sh -->

    $ keytool -import -v -trustcacerts -alias 0 \
    -file *PATH_TO_SELF_CERT.PEM* \
    -keystore *PATH_TO_KEYSTORE* \
    -storetype BKS \
    -provider org.bouncycastle.jce.provider.BouncyCastleProvider \
    -providerpath *PATH_TO_bcprov-jdk15on-146.jar* \
    -storepass *STOREPASS*

PATH_TO_KEYSTORE 指向将在其中创建您的密钥库的文件。它不得存在

PATH_TO_bcprov-jdk15on-146.jar.JAR 是下载的 .jar 库的路径。

STOREPASS 是您新创建的密钥库密码。

    在您的应用程序中包含 KeyStore

将您新创建的密钥库从 PATH_TO_KEYSTORE 复制到 res/raw/certs.bkscerts.bks 只是文件名;您可以使用任何您想要的名称)

res/values/strings.xml 中创建一个密钥

<!-- language: lang-xml -->

    <resources>
    ...
        <string name="store_pass">*STOREPASS*</string>
    ...
    </resources>

    创建一个继承DefaultHttpClient的此类

    import android.content.Context;
    import android.util.Log;
    import org.apache.http.conn.scheme.PlainSocketFactory;
    import org.apache.http.conn.scheme.Scheme;
    import org.apache.http.conn.scheme.SchemeRegistry;
    import org.apache.http.conn.ssl.SSLSocketFactory;
    import org.apache.http.impl.client.DefaultHttpClient;
    import org.apache.http.params.HttpParams;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.security.*;
    
    public class MyHttpClient extends DefaultHttpClient 
    
        private static Context appContext = null;
        private static HttpParams params = null;
        private static SchemeRegistry schmReg = null;
        private static Scheme httpsScheme = null;
        private static Scheme httpScheme = null;
        private static String TAG = "MyHttpClient";
    
        public MyHttpClient(Context myContext) 
    
            appContext = myContext;
    
            if (httpScheme == null || httpsScheme == null) 
                httpScheme = new Scheme("http", PlainSocketFactory.getSocketFactory(), 80);
                httpsScheme = new Scheme("https", mySSLSocketFactory(), 443);
            
    
            getConnectionManager().getSchemeRegistry().register(httpScheme);
            getConnectionManager().getSchemeRegistry().register(httpsScheme);
    
        
    
        private SSLSocketFactory mySSLSocketFactory() 
            SSLSocketFactory ret = null;
            try 
                final KeyStore ks = KeyStore.getInstance("BKS");
    
                final InputStream inputStream = appContext.getResources().openRawResource(R.raw.certs);
    
                ks.load(inputStream, appContext.getString(R.string.store_pass).toCharArray());
                inputStream.close();
    
                ret = new SSLSocketFactory(ks);
             catch (UnrecoverableKeyException ex) 
                Log.d(TAG, ex.getMessage());
             catch (KeyStoreException ex) 
                Log.d(TAG, ex.getMessage());
             catch (KeyManagementException ex) 
                Log.d(TAG, ex.getMessage());
             catch (NoSuchAlgorithmException ex) 
                Log.d(TAG, ex.getMessage());
             catch (IOException ex) 
                Log.d(TAG, ex.getMessage());
             catch (Exception ex) 
                Log.d(TAG, ex.getMessage());
             finally 
                return ret;
            
        
    
    

现在只需像使用 **DefaultHttpClient** 一样使用 **MyHttpClient** 的实例来进行 HTTPS 查询,它将正确使用和验证您的自签名 SSL 证书。

HttpResponse httpResponse;

HttpPost httpQuery = new HttpPost("https://yourserver.com");
... set up your query ...

MyHttpClient myClient = new MyHttpClient(myContext);

try

    httpResponse = myClient.(peticionHttp);

    // Check for 200 OK code
    if (httpResponse.getStatusLine().getStatusCode() == HttpURLConnection.HTTP_OK) 
        ... do whatever you want with your response ...
    

catch (Exception ex)
    Log.d("httpError", ex.getMessage());

【讨论】:

我不敢相信有人使用了相同的 (DefaultHttpClient)。我真的很感谢你的努力,我会尝试用我的 Android 项目来调整它。谢谢!!! @CarlosAlbertoMartínezGadea proximamente actualizaré la clase completa, con algunas mejoras。 ||很快我会更新我的课程并进行一些改进。 我使用了一个与您的非常相似的类,并进行了一些更新。来自***.com/questions/4065379/… 使用通过不安全连接下载的程序为安全连接创建密钥库没有抓住重点。 @FredrikPortström 我不明白,我可以通过 ssh 下载 BouncyCastle 但仍然不安全,关键是要保证应用程序之间的安全性服务器通道... 【参考方案4】:

除非我遗漏了什么,否则此页面上的其他答案都是危险的,并且在功能上等同于根本不使用 SSL。如果您信任自签名证书而不进行进一步检查以确保证书是您期望的证书,那么任何人都可以创建自签名证书并可以伪装成您的服务器。到那时,你就没有真正的安全感了。

执行此操作的唯一合法方法(无需编写完整的 SSL 堆栈)是在证书验证过程中添加额外的受信任锚点以受信任。两者都涉及将受信任的锚点证书硬编码到您的应用中,并将其添加到操作系统提供的任何受信任的锚点中(否则,如果您获得真正的证书,您将无法连接到您的网站)。

我知道有两种方法可以做到这一点:

    按照http://www.ibm.com/developerworks/java/library/j-customssl/#8 中的说明创建自定义信任库

    创建 X509TrustManager 的自定义实例并覆盖 getAcceptedIssuers 方法以返回包含您的证书的数组:

    public X509Certificate[] getAcceptedIssuers()
    
        X509Certificate[] trustedAnchors =
            super.getAcceptedIssuers();
    
        /* Create a new array with room for an additional trusted certificate. */
        X509Certificate[] myTrustedAnchors = new X509Certificate[trustedAnchors.length + 1];
        System.arraycopy(trustedAnchors, 0, myTrustedAnchors, 0, trustedAnchors.length);  
    
        /* Load your certificate.
    
           Thanks to http://***.com/questions/11857417/x509trustmanager-override-without-allowing-all-certs
           for this bit.
         */
        InputStream inStream = new FileInputStream("fileName-of-cert");
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        X509Certificate cert = (X509Certificate)cf.generateCertificate(inStream);
        inStream.close();
    
        /* Add your anchor cert as the last item in the array. */
        myTrustedAnchors[trustedAnchors.length] = cert;
    
        return myTrustedAnchors;
    
    

请注意,此代码完全未经测试,甚至可能无法编译,但至少应该引导您朝着正确的方向前进。

【讨论】:

最近有一些关于安卓开发者不检查他们的证书的新闻。所以这看起来不错。 好吧,原来 X509TrustManager 是一个接口,所以你不能继承它。 您可以创建自己的信任管理器。 publib.boulder.ibm.com/infocenter/javasdk/v6r0/… 要添加到@Peterdk 的第一条评论:参见this paper,名为“Why Eve and Mallory Love Android: An Analysis of Android SSL (In)Security”。 这有多大必要,真的吗?似乎这应该是常见的做法。【参考方案5】:

如果您修改较大的 createSocket 方法重载,Brian Yager 的答案也适用于 Android 2.2,如下所示。我花了一段时间才让自签名 SSL 工作。

public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException 
    return getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose);

【讨论】:

【参考方案6】:

在 Android 上,HttpProtocolParams 接受 ProtocolVersion 而不是 HttpVersion

ProtocolVersion pv = new ProtocolVersion("HTTP", 1, 1);
HttpProtocolParams.setVersion(params, pv);

【讨论】:

【参考方案7】:

@Chris - 因为我还不能添加 cmets,所以将其发布为答案。我想知道您的方法是否应该在使用 webView 时起作用。我在 Android 2.3 上无法做到这一点 - 相反,我只是得到一个白屏。

经过一番搜索,我发现了这个simple fix for handling SSL errors in a webView,它对我来说就像一个魅力。

在处理程序中,我检查我是否处于特殊的开发模式并调用 handler.proceed(),否则我调用 handler.cancel()。这使我可以在本地网站上针对自签名证书进行开发。

【讨论】:

【参考方案8】:

这个用例有很多替代方案。如果您不想在代码库中包含任何自定义代码,例如自定义 TrustManager,我建议您尝试 GitHub - SSLContext Kickstart 和以下代码 sn-p:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>sslcontext-kickstart</artifactId>
    <version>6.7.0</version>
</dependency>

SSL 配置

SSLFactory sslFactory = SSLFactory.builder()
    .withUnsafeTrustMaterial()
    .build();

SSLContext sslContext = sslFactory.getSslContext();
SSLSocketFactory sslSocketFactory = sslFactory.getSslSocketFactory();

HttpClient 配置

HttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);

SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("https", sslSocketFactory, 443));

ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);

HttpClient httpClient = new DefaultHttpClient(ccm, params);

HttpsUrlConnection

HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory); 

【讨论】:

以上是关于Android 上的自签名 SSL 接受的主要内容,如果未能解决你的问题,请参考以下文章

ini MacOS Sierra和Chrome上的自签名SSL证书

如何在 WCF 客户端中接受自签名 SSL 证书?

如何创建用于 Tomcat 的自签名 SSL 证书?

Mac OS X - 接受自签名多域 SSL 证书

如何信任 Android 上的自签名证书?

App Store 是不是接受自签名证书应用程序?