Android 安全 - 为您的应用实现自签名 SSL 证书。






4.93/5 (11投票s)
引言
安全性已成为一个非常大的问题。如果您在服务器上存储任何用户数据,您应该认真考虑使用 SSL 加密客户端和服务器之间的所有通信。近年来,移动应用程序呈指数级增长。如今,攻击者/黑客主要关注移动应用程序,原因如下:
- 缺乏良好的移动标准
- 开发者对移动应用开发来说很新
- 更重要的是,通过使用移动应用程序,黑客可以访问日历、联系人、浏览器历史记录、个人资料信息、社交动态、短信或确切地理位置等重要内容。
作为一名安全爱好者,我近几个月分析了 50 到 60 个 Android 应用程序。我在安全验证方面发现了许多安全漏洞。
在本文中,我想解释一些有关如何在 Android 应用程序中编写安全代码的技巧。
背景
让我们从基础开始。
在阅读本文之前,请先阅读 Ranjan.D 的文章(https://codeproject.org.cn/Articles/818734/Article-Android-Connectivity)。它将提供有关 Android 连接性的简要介绍。
您可以使用以下代码打开 HTTP 连接
URL urlConnection = new URL("https://codeproject.org.cn/"); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
但是,使用 http,您无法执行所有操作,例如登录页面或传递用户名和密码或信用卡信息。您需要使用 HTTPS(有关 HTTPS 的更多信息)。
什么是 HTTPS?
超文本传输协议安全 (HTTPS) 是超文本传输协议 (http) 的安全版本。**HTTPS** 支持安全的电子商务交易,例如网上银行。
Internet Explorer 和 Firefox 等网页浏览器会显示一个挂锁图标,表示网站是安全的,并且地址栏也会显示 https://。
https 和 http 之间最简单的区别是 https 在通信中增加了一个额外的层,因此提供了额外的安全性。
HTTPS 连接打开
URL urlConnection = new URL("https://codeproject.org.cn/"); HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); InputStream in = new BufferedInputStream(urlConnection.getInputStream());
通过这种方式,我们可以安全地打开 HTTPS 连接并传输数据。
HTTPS 将使用 SSL/TLS 机制来传输数据。
SSL/TLS
SSL(安全套接层)是一种标准安全技术,用于在服务器和客户端之间建立加密链接——通常是 Web 服务器(网站)和浏览器;或者邮件服务器和邮件客户端(例如 Outlook)。
SSL 允许安全地传输敏感信息,如信用卡号、社会安全号码和登录凭据。通常,在浏览器和 Web 服务器之间发送的数据是以纯文本形式发送的——这会使您容易受到窃听。如果攻击者能够截获浏览器和 Web 服务器之间发送的所有数据,他们就可以看到并使用这些信息。
SSL/HTTPS 和 X.509 证书概述
如果您对 SSL 或 X.509 证书一无所知,一个简要的解释可能会有所帮助。任何使用自签名证书的人都应该了解它与购买的证书有何不同,尤其是在潜在的安全风险方面。那么 SSL 证书是什么?通常有两点:1)一种身份证明形式,类似于护照或驾驶执照;2)一个公钥加密密钥,可用于加密数据,以便**只有证书所有者**才能解密它。换句话说,SSL 证书有两个目的:识别使用该证书的网站并与其进行安全通信。
下一个重要的概念是,一个证书可以用来“签名”其他证书。通俗地说,Bob 可以使用他的证书为其他证书盖上“认可戳”;如果您信任 Bob(以及他的证书),那么您就可以信任他签名的任何证书。在这种情况下,Bob 被称为“证书颁发机构”。所有主流浏览器都附带了一系列受信任的证书颁发机构的证书(再次,Thawte 和 Verisign 是常见的例子)。
最后一步是理解浏览器如何使用证书。从高层次来看,当您在浏览器中打开“https://www.yoursite.com”时会发生以下情况:
- Web 服务器会将证书发送给浏览器。
- 浏览器将证书中的“通用名称”(有时称为“主体”)与服务器的域名进行比较。例如,从“www.yoursite.com”发送的证书**必须**具有“www.yoursite.com”的通用名称,否则浏览器将显示警告消息,称其不可信。
- 浏览器尝试验证它是否可以信任该证书。这就像一个保镖检查您的身份证是否有全息图来证明其真实性。正如任何人都可以制作假身份证并试图窃取您的身份一样,有人也可以创建“伪造”的证书,其通用名称为“www.yoursite.com”,**看起来**属于您的网站。为了确认证书可信,浏览器会检查它是否已被其信任的证书颁发机构集合中的任何证书签名。如果浏览器找不到与证书上的“认可戳”匹配的受信任“认可戳”,那么它会向用户显示一个警告消息,表明证书不可信。**请注意,用户可以选择忽略警告并接受证书。**
- 一旦证书验证通过(或者用户忽略了警告并指示浏览器接受它),浏览器就会开始使用证书中的公钥加密数据,并将该数据发送到服务器。
TLS (SSL) 中的加密
一旦客户端验证了服务器的身份,它就会选择一个共享密钥,并使用服务器的公钥(来自服务器证书)对其进行加密。它将加密的共享密钥发送到服务器。共享密钥只能使用服务器的私钥(非对称加密)解密,因此可以防止窃听。服务器在会话期间保存此共享密钥。会话中的所有后续通信都使用此共享密钥(对称加密)进行加密和解密。
理论部分已完成;现在让我们看一些示例来清楚地理解上述理论。
如果您在浏览器中打开 mail.live.com,您可以在浏览器 URL 栏中看到绿色图标符号和 https。
点击那个绿色图标按钮,然后选择证书信息链接。
您会看到以下屏幕
这是 SSL 证书,您可以看到上面的证书是由 Verisign 颁发给“mail.live.com”的。
在这里,Verisign 是证书颁发机构,它告诉您的浏览器您正在连接到 mail.live.com 网站。因此,浏览器确保浏览器与 Web 服务器之间建立了安全连接。这样我们就可以避免中间人攻击。
MITM 攻击
在 MITM 攻击 (MITMA) 中,攻击者能够拦截通信双方之间的消息。
在被动 MITMA 中,攻击者只能窃听通信,而在主动 MITMA 中,攻击者还可以篡改通信(攻击者标签:Mallory)。与传统台式计算机相比,针对移动设备的 MITMA 稍微容易执行,因为移动设备的使用经常发生在不断变化且不可信的环境中。具体来说,使用开放接入点和邪恶双胞胎攻击。
CA 提供商
· Symantec(收购了VeriSign 的 SSL 业务,拥有 Thawte 和 Geotrust)占有 38.1% 的市场份额
· Comodo SSL 占有 29.1%
· Go Daddy 占有 13.4%
· GlobalSign 占有 10%
在 Android Jelly Bean 版本中,您可以找到受信任的 CA 列表。通过导航到
设置 -> 安全 -> 受信任的凭据。
HTTPS 连接
URL url = new URL("https://www.example.com/"); HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection(); InputStream in = urlConnection.getInputStream();
如果您的连接服务器(www.example.com)拥有 CA 证书,这将完美运行。
但是,如果您的连接服务器使用自签名证书,问题就会出现。
什么是自签名证书?
自签名证书是由创建它的人签名的证书,而不是由受信任的证书颁发机构签名的。
您可以大致将 SSL 证书分为三种类型:
- 由 Android 认可的证书颁发机构 (CA)(例如 VeriSign)颁发的证书,或由其上游 CA 为 Android 认可的下游 CA 颁发的证书。
- 由 Android 未认可的 CA 颁发的证书。
- 自签名证书,无论是在开发期间(例如)还是在生产环境中使用。
Android 只能透明地处理第一类,其中证书的根 CA 是 Android 认可的。
为什么人们会使用自签名证书?
- 自签名证书是免费的。如果您需要由受信任的 CA 颁发的证书,您需要每年支付大约 100 美元到 500 美元。
- 自签名证书在移动开发中非常常见(因为您的移动应用程序只会与一个服务器交互,这与传统浏览器不同)。
- 我们可以在测试和开发中使用相同的代码,人们将使用自签名证书。
在最近的一项调查中,在 810 万个唯一证书中,有 320 万个是浏览器信任的。其余 490 万个不受信任的证书是自签名证书 (48%)、由未知发行者签名的证书 (33%) 和由已知但不受信任的发行者签名的证书 (19%) 的组合。
不仅在此调查中,在我进行的分析中,我也发现排名前 60% 的 Android 应用程序都在使用自签名证书。
我个人认为自签名证书对移动应用程序来说效果很好,因为您无需购买 CA 证书,并且可以在开发和生产环境中使用相同的代码(注意:如果您在生产环境中使用 CA 证书,则需要更改代码)。
但这里的诀窍是通用的 https 代码。
URL url = new URL("https://www.example.com/"); HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); InputStream in = urlConnection.getInputStream();
如果您使用上述代码来验证自签名证书,Android 应用程序将抛出错误,因为自签名证书未被 Android OS 验证。因此,您需要编写自己的代码来检查自签名证书。
但是,在这方面 Android 开发者犯了很大的错误。自签名证书在 Web 开发中并不常见,大多数 Android 开发者都来自 Web 开发背景,因此开发者对加密概念的了解很少。
在我进行分析的过程中,我看到开发者只是从 Stack Overflow 和其他博客上复制粘贴答案,这些答案会让您的应用默认信任所有证书。即使大多数答案都说这只应该在测试模式下进行,但开发者还是照搬代码。这导致应用程序容易受到中间人攻击和会话劫持攻击。
示例:
http://stackoverflow.com/questions/2012497/accepting-a-certificate-for-https-on-android?lq=1
http://www.caphal.com/android/using-self-signed-certificates-in-android/#toc_3
http://stackoverflow.com/questions/2642777/trusting-all-certificates-using-httpclient-over-https
实现自签名证书时的常见错误
信任所有证书。
TrustManager
的主要职责是确定提供的身份验证凭据是否应被信任。如果凭据不受信任,连接将被终止。要验证安全套接字对端的远程身份,您需要使用一个或多个 TrustManager
来初始化 SSLContext
对象。
import org.apache.http.conn.ssl.SSLSocketFactory; public class MySSLSocketFactory extends SSLSocketFactory { SSLContext sslContext = SSLContext.getInstance("TLS"); public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { super(truststore); TrustManager tm = new X509TrustManager() { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public X509Certificate[] getAcceptedIssuers() { return null; } }; sslContext.init(null, new TrustManager[] { tm }, null); } @Override public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException { return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose); } @Override public Socket createSocket() throws IOException { return sslContext.getSocketFactory().createSocket(); } }
在我分析的 80 到 100 个应用程序中,我观察到 20 到 25 个应用程序中存在上述代码实现。
上述 TrustManager 接口可以实现为信任所有证书,而不管谁签名的,甚至是为了什么主题颁发的。此接口允许接受**任何**证书。接受任何证书都可能危及数据完整性、安全性等。
在上面的示例中,checkClientTrusted、getAcceptedIssuers、checkServerTrusted 是三个重要的函数。每个开发者都应该注意这三个函数的实现。但有些开发者只是在网上搜索并从不同网站复制这些函数。
允许所有主机名。
有可能忘记检查证书是否是为该地址颁发的,或者
不,即访问服务器 example.com 时,接受了为 some-other-domain.com 颁发的证书。
HostnameVerifier hostnameVerifier = org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; DefaultHttpClient client = new DefaultHttpClient(); SchemeRegistry registry = new SchemeRegistry(); SSLSocketFactory socketFactory = SSLSocketFactory.getSocketFactory(); socketFactory.setHostnameVerifier((X509HostnameVerifier) hostnameVerifier); registry.register(new Scheme("https", socketFactory, 443)); SingleClientConnManager mgr = new SingleClientConnManager(client.getParams(), registry); DefaultHttpClient httpClient = new DefaultHttpClient(mgr, client.getParams()); // Set verifier HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier); // Example send http request final String url = "https://www.paypal.com” HttpPost httpPost = new HttpPost(url); HttpResponse response = httpClient.execute(httpPost); HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
HostnameVerifier hostnameVerifier = org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;
上面的代码将接受颁发给任何域的任何 CA 证书。这是一个错误的实现。
混合模式/无 SSL。
应用程序开发人员可以自由地在同一应用程序中混合安全和不安全的连接,或者根本不使用 SSL。这并非直接是 SSL 问题,但值得一提的是,普通应用程序用户没有外部迹象,也无法检查是否正在使用安全连接。这为 SSL 剥离等攻击或 Firesheep 等工具打开了大门。
SSL 剥离是另一种针对 SSL 连接发起 MITMA 的方法,它利用了使用 HTTP 和 HTTPS 混合连接的应用程序。SSL 剥离依赖于这样一个事实:许多 SSL 连接是通过单击链接或从非 SSL 保护的站点重定向建立的。在 SSL 剥离期间,Mallory 将非保护站点中的 https:// 链接替换为不安全的 http:// 链接。因此,除非用户注意到链接已被篡改,否则 Mallory 可以完全绕过 SSL 保护。此攻击主要适用于浏览器应用程序或使用 Android WebView 的应用程序。
有关 SSL 剥离的更多信息
http://security.stackexchange.com/questions/41988/how-does-sslstrip-work
http://www.thoughtcrime.org/software/sslstrip/
使用自签名证书进行证书固定
这意味着将服务器使用的已知证书硬编码在移动应用程序中。然后,应用程序可以忽略设备的信任存储,而依赖于自己的信任存储,并且只允许与应用程序内部存储的证书签名的主机建立 SSL 连接。
这也有可能信任一个带有自签名证书的主机,而无需在设备上安装额外的证书。
优点
- **安全性增强** - 使用固定 SSL 证书后,应用程序独立于设备的信任存储。要破解应用程序中硬编码的信任存储并不容易——应用程序需要被反编译、修改然后重新编译——而且它不能使用原始应用程序开发人员使用的相同 Android 密钥库进行签名。
- **降低成本** - SSL 证书固定使您能够使用可信的自签名证书。例如,您正在开发一个使用您自己的 API 服务器的应用程序。您可以通过在服务器上使用自签名证书(并在应用程序中固定该证书)来降低成本,而不是为证书付费。虽然有些繁琐,但您实际上提高了安全性并节省了一些钱。
缺点
- **灵活性较低** - 进行 SSL 证书固定时,更改 SSL 证书并不容易。每次更改 SSL 证书,都必须更新应用程序,推送到 Google Play,并希望用户会安装它。
在我们的情况下,证书是自签名的。这意味着我们的 SSLContext 中的默认TrustManager 将不信任服务器证书,并且 SSL 连接将失败。为避免此问题,我们将设置一个自定义 TrustManager 来信任我们的自签名证书,并将该 TrustManager 提供给我们的自定义 SSLContext。
我们将证书加载到 KeyStore 中,使用该 KeyStore 生成一个 TrustManager 数组,然后使用这些 TrustManager 来创建 SSLContext。
在我们的应用程序中,我们将服务器证书文件包含在应用程序资源中(因为它不因用户而异,并且更改频率不高),但您也可以将其放在外部文件中。
实施
步骤 1:创建您的自签名证书并创建 .bks 文件
1)
要创建 BKS 或密钥库,您需要 bcprov-jdk15on-146.jar 文件,此类将为我们完成所有工作,有不同的版本,但这一个对我有效 http://www.bouncycastle.org/download/bcprov-jdk15on-146.jar 另请将此文件保存在 C:\codeproject。
现在您将使用 Keytool(keytool 随 Java SDK 一起提供。您应该在包含 javac 的目录中找到它)来生成我们的密钥库,并确保它正在工作,请转到您的 cmd 并输入“Keytool”,您将看到可用的命令,这意味着它正在工作,或者您可以访问“C:\Program Files (x86)\Java\jre7\bin>keytool”。
2)
使用 keytool 生成您的密钥。它位于您的 Java bin 路径中。如果您已经生成了密钥库文件,则可以跳过第一个命令。
keytool -genkey -alias codeproject -keystore C:\codeproject\codeprojectssl.keystore -validity 365
它会创建一个别名为 code project 的密钥,文件名为 codeprojectssl.keystore。它会询问有关密钥和密钥库的密码,以及 SSL 详细信息。请注意,通用名称将是您的主机名,在本例中为:codeproject.com。
3)
keytool -export -alias codeproject -keystore C:\codeproject\codeprojectssl.keystore -file C:\codeproject\codeprojectsslcert.cer
此命令将 .keystore 文件中的密钥导出到 .cer 文件。
4)
keytool -import -alias codeproject -file C:\codeproject\codeprojectsslcert.cer -keystore C:\codeproject\codeprojectssl.bks -storetype BKS -providerClass org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath C:\codeproject\bcprov-jdk15on-146.jar
成功!我们获得了 .bks 文件,该文件将放入我们的 Android 应用程序中,这将允许我们的应用程序与我们拥有自签名 SSL 证书的服务器进行通信。
步骤 2
我们需要将我们的 .keystore 文件放在** /androidappdir/res/raw/**
步骤 3
我们将编写一个名为 MyHttpClient 的新类,它将扩展 DefaultHttpClient。此类将加载我们自己的信任存储来检查 SSL 证书,而不是 Android 的默认信任存储。只有当证书在这里与服务器上的证书匹配时,它才会正确工作。以下是它的样子:
import java.io.InputStream; import java.security.KeyStore; import android.content.Context; public class MyHttpClient extends DefaultHttpClient { private static Context context; public static void setContext(Context context) { MyHttpClient.context = context; } public MyHttpClient(HttpParams params) { super(params); } public MyHttpClient(ClientConnectionManager httpConnectionManager, HttpParams params) { super(httpConnectionManager, params); } @Override protected ClientConnectionManager createClientConnectionManager() { SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); // Register for port 443 our SSLSocketFactory with our keystore // to the ConnectionManager registry.register(new Scheme("https", newSslSocketFactory(), 443)); return new SingleClientConnManager(getParams(), registry); } private SSLSocketFactory newSslSocketFactory() { try { // Get an instance of the Bouncy Castle KeyStore format KeyStore trusted = KeyStore.getInstance("BKS"); // Get the raw resource, which contains the keystore with // your trusted certificates (root and any intermediate certs) InputStream in = MyHttpClient.context.getResources().openRawResource(R.raw.codeprojectssl); //name of your keystore file here try { // Initialize the keystore with the provided trusted certificates // Provide the password of the keystore trusted.load(in, "YourKeystorePassword".toCharArray()); } finally { in.close(); } // Pass the keystore to the SSLSocketFactory. The factory is responsible // for the verification of the server certificate. SSLSocketFactory sf = new SSLSocketFactory(trusted); // Hostname verification from certificate // http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e506 sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER); // This can be changed to less stricter verifiers, according to need return sf; } catch (Exception e) { throw new AssertionError(e); } } }
您可以通过这种方式调用 myhttpclient:
// Instantiate the custom HttpClient DefaultHttpClient client = new MyHttpClient(getApplicationContext()); HttpGet get = new HttpGet("https://www.google.com"); // Execute the GET call and obtain the response HttpResponse getResponse = client.execute(get); HttpEntity responseEntity = getResponse.getEntity();
这是我在 CodeProject 上的第一篇文章。
祝您编写安全代码愉快 :)。
参考
http://www.thoughtcrime.org/blog/authenticity-is-broken-in-ssl-but-your-app-ha/
http://security.stackexchange.com/questions/29988/what-is-certificate-pinning
https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning
https://tools.ietf.org/html/draft-ietf-websec-key-pinning-20
https://media.blackhat.com/bh-us12/Turbo/Diquet/BH_US_12_Diqut_Osborne_Mobile_Certificate_Pinning_Slides.pdf
http://docs.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#HowSSLWorks