原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY-NC-SA 4.0
在本节中,将介绍如何在 Linux(如 Ubuntu 和 CentOS)中创建私有证书和配置服务器。 私有证书是指私人签发的服务器证书,并由 Cybertrust 和 VeriSign 等可信第三方证书机构签发的服务器证书通知。
创建私有证书机构
首先,你需要创建一私有证书机构来颁发私有证书。 私有证书机构是指私有创建的证书机构以及私有证书。 你可以使用单个私有证书机构颁发多个私有证书。 存储私有证书机构的个人电脑应严格限制为只能由可信的人访问。
为了创建私有证书机构,必须创建两个文件,例如以下 shell 脚本newca.sh
和设置文件openssl.cnf
,然后执行它们。 在 shell 脚本中,CASTART
和CAEND
代表证书机构的有效期,CASUBJ
代表证书机构的名称。 所以这些值需要根据你创建的证书机构进行更改。 在执行 shell 脚本时,访问证书机构的密码总共需要 3 次,所以你需要每次都输入它。
newca.sh – 创建证书机构的 Shell 脚本
#!/bin/bash
umask 0077
CONFIG=openssl.cnf
CATOP=./CA
CAKEY=cakey.pem
CAREQ=careq.pem
CACERT=cacert.pem
CAX509=cacert.crt
CASTART=130101000000Z # 2013/01/01 00:00:00 GMT
CAEND=230101000000Z # 2023/01/01 00:00:00 GMT
CASUBJ="/CN=JSSEC Private CA/O=JSSEC/ST=Tokyo/C=JP"
mkdir -p ${CATOP}
mkdir -p ${CATOP}/certs
mkdir -p ${CATOP}/crl
mkdir -p ${CATOP}/newcerts
mkdir -p ${CATOP}/private
touch ${CATOP}/index.txt
openssl req -new -newkey rsa:2048 -sha256 -subj "${CASUBJ}" ¥
-keyout ${CATOP}/private/${CAKEY} -out ${CATOP}/${CAREQ}
openssl ca -selfsign -md sha256 -create_serial -batch ¥
-keyfile ${CATOP}/private/${CAKEY} ¥
-startdate ${CASTART} -enddate ${CAEND} -extensions v3_ca ¥
-in ${CATOP}/${CAREQ} -out ${CATOP}/${CACERT} ¥
-config ${CONFIG}
openssl x509 -in ${CATOP}/${CACERT} -outform DER -out ${CATOP}/${CAX509}
openssl.cnf – 2 个 shell 脚本共同参照的 openssl 命令的设置文件
[ ca ]
default_ca = CA_default # The default ca section
[ CA_default ]
dir = ./CA # Where everything is kept
certs = $dir/certs # Where the issued certs are kept
crl_dir = $dir/crl # Where the issued crl are kept
database = $dir/index.txt # database index file.
#Proprietary-defined _subject = no # Set to 'no' to allow creation of
# several ctificates with same subject.
new_certs_dir = $dir/newcerts # default place for new certs.
certificate = $dir/cacert.pem # The CA certificate
serial = $dir/serial # The current serial number
crlnumber = $dir/crlnumber # the current crl number
# must be commented out to leave a V1 CRL
crl = $dir/crl.pem # The current CRL
private_key = $dir/private/cakey.pem# The private key
RANDFILE = $dir/private/.rand # private random number file
x509_extensions = usr_cert # The extentions to add the cert
name_opt = ca_default # Subject Name options
cert_opt = ca_default # Certificate field options
policy = policy_match
[ policy_match ]
countryName = match
stateOrProvinceName = match
organizationName = supplied
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ usr_cert ]
basicConstraints=CA:FALSE
nsComment = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = CA:true
创建私有证书
为了创建私有证书,你必须创建一个 shell 脚本并执行它,像下面的newca.sh
一样。 在 shell 脚本中,SVSTART
和SVEND
代表私有证书的有效期,SVSUBJ
代表 Web 服务器的名称,所以这些值需要根据目标 Web 服务器而更改。 尤其是,你需要确保不要将错误的主机名设置为SVSUBJ
的/CN
,它指定了 Web 服务器主机名。 在执行 shell 脚本时,会询问访问证书机构的密码,因此你需要输入你在创建私有证书机构时设置的密码。 之后,y / n
总共被询问 2 次,每次需要输入y
。
newsv.sh – 签发私有证书的 Shell 脚本
#!/bin/bash
umask 0077
CONFIG=openssl.cnf
CATOP=./CA
CAKEY=cakey.pem
CACERT=cacert.pem
SVKEY=svkey.pem
SVREQ=svreq.pem
SVCERT=svcert.pem
SVX509=svcert.crt
SVSTART=130101000000Z # 2013/01/01 00:00:00 GMT
SVEND=230101000000Z # 2023/01/01 00:00:00 GMT
SVSUBJ="/CN=selfsigned.jssec.org/O=JSSEC Secure Cofing Group/ST=Tokyo/C=JP"
openssl genrsa -out ${SVKEY} 2048
openssl req -new -key ${SVKEY} -subj "${SVSUBJ}" -out ${SVREQ}
openssl ca -md sha256 ¥
-keyfile ${CATOP}/private/${CAKEY} -cert ${CATOP}/${CACERT} ¥
-startdate ${SVSTART} -enddate ${SVEND} ¥
-in ${SVREQ} -out ${SVCERT} -config ${CONFIG}
openssl x509 -in ${SVCERT} -outform DER -out ${SVX509}
执行上面的 shell 脚本后,Web 服务器的svkey.pem
(私钥文件)和svcert.pem
(私有证书文件)都在工作目录下生成。 当 Web 服务器是 Apache 时,你将在配置文件中指定prikey.pem
和cert.pem
,如下所示。
SSLCertificateFile "/path/to/svcert.pem"
SSLCertificateKeyFile "/path/to/svkey.pem"
在示例代码“5.4.1.3 通过使用私有证书的 HTTPS 进行通信”中,介绍了通过将根证书安装到应用中,使用私有证书建立应用到 Web 服务器的 HTTPS 会话的方法。 本节将介绍通过将根证书安装到 Android OS 中,建立使用私有证书的所有应用到 Web 服务器的 HTTPS 会话的方法。 请注意,你安装的所有东西,应该是由可信证书机构颁发的证书,包括你自己的证书机构。
首先,你需要将根证书文件cacert.crt
复制到 Android 设备的内部存储器中。 你也可以从 https://selfsigned.jssec.org/cacert.crt 获取示例代码中使用的根证书文件。
然后,你将从 Android 设置中打开安全页面,然后你可以按如下方式在 Android 设备上安装根证书。
在 Android 操作系统中安装根证书后,所有应用都可以正确验证证书机构颁发的每个私有证书。 下图显示了在 Chrome 浏览器中显示 https://selfsigned.jssec.org/droid_knight.png 时的示例。
通过以这种方式安装根证书,即使是使用示例代码“5.4.1.2 通过 HTTPS 通信”的应用,也可以通过 HTTPS 正确连接到使用私有证书操作的 Web 服务器。
互联网上发现了很多不正确的示例(代码片段),它们允许应用在证书验证错误发生后,通过 HTTPS 与 Web 服务器继续通信。 由于它们作为一种方式而引入,通过 HTTPS 与使用私有证书的 Web 服务器进行通信,因此开发人员通过复制和粘贴使用这些示例代码,创建了许多应用。 不幸的是,他们中的大多数容易受到中间人攻击。 正如本文前面所述,“2012 年,Android 应用中 HTTPS 通信实现中的许多缺陷被指出”,许多 Android 应用已经实现了这种易受攻击的代码。
下面显示了 HTTPS 通信的几个存在漏洞的代码片段。 当你找到此类代码片段时,强烈建议替换为“5.4.1.3 通过 HTTPS 与私有证书进行通信”的示例代码。
风险:创建空TrustManager
时的情况
TrustManager tm = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
// Do nothing -> accept any certificates
}
@Override
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
// Do nothing -> accept any certificates
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
风险:创建空HostnameVerifier
时的情况
HostnameVerifier hv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
// Always return true -> Accespt any host names
return true;
}
};
风险:使用ALLOW_ALL_HOSTNAME_VERIFIER
的情况
SSLSocketFactory sf;
[...]
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
如果你希望为 HTTP 或 HTTPS 通信指定你自己的单个 HTTP 请求头,请使用URLConnection
类中的setRequestProperty()
或addRequestProperty()
方法。 如果你使用从外部来源接收的输入数据作为这些方法的参数,则必须实施 HTTP 协议头注入保护。 HTTP 协议头注入攻击的第一步,是在输入数据中包含回车代码(在 HTTP 头中用作分隔符)。 因此,必须从输入数据中删除所有回车代码。
配置 HTTP 请求头
public byte[] openConnection(String strUrl, String strLanguage, String strCookie) {
// HttpURLConnection is a class derived from URLConnection
HttpURLConnection connection;
try {
URL url = new URL(strUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
// *** POINT *** When using input values in HTTP request headers,
// check the input data in accordance with the application's requirements
// (see Section 3.2: Handling Input Data Carefully and Securely)
if (strLanguage.matches("^[a-zA-Z ,-]+$")) {
connection.addRequestProperty("Accept-Language", strLanguage);
} else {
throw new IllegalArgumentException("Invalid Language : " + strLanguage);
}
// *** POINT *** Or URL-encode the input data (as appropriate for the purposes of the app in
queestion)
connection.setRequestProperty("Cookie", URLEncoder.encode(strCookie, "UTF-8"));
connection.connect();
[...]
当应用使用 HTTPS 通信时,在通信开始时执行的握手过程中的一个步骤是,检查从远程服务器发送的证书是否由第三方证书机构签署。但是,攻击者可能会从第三方认证代理获取不合适的证书,或者可能从证书机构获取签署的密钥来构造不合适的证书。在这种情况下,应用将无法在握手过程中检测到攻击,即使在攻击者建立不正确的服务器或中间人攻击的情况下也是如此 - 因此, ,可能会造成损失。
固定技术是一种有效的策略,可以防止不正当的第三方证书机构使用这些类型的证书,来进行中间人攻击。在这种方法中,远程服务器的证书和公钥被预先存储在一个应用中,并且这个信息用于握手过程,以及握手过程完成后的重新测试。
如果第三方证书机构(公钥基础设施的基础)的可信度受到损害,则可以使用固定来恢复通信的安全性。 应用开发人员应评估自己的应用处理的资产级别,并决定是否实现这些测试。
在握手过程中使用存储在应用中的证书和公钥
为了在握手过程中,使用存储在应用中的远程服务器证书或公钥中包含的信息,应用必须创建包含此信息的,自己的KeyStore
并在通信时使用它。 如上所述,即使在使用来自不正当的第三方证书机构的证书的,中间人攻击的情况下,这也将允许应用检测握手过程中的不当行为。 请参阅“5.4.1.3 使用 HTTPS 与私有证书进行通信”一节中介绍的示例代码,了解建立应用自己的KeyStore
来执行 HTTPS 通信的详细方法。
握手过程完成后,使用应用中存储的证书和公钥信息进行重新测试
为了在握手过程完成后重新测试远程服务器,应用首先会获得证书链,它在握手过程中受到系统测试和信任,然后比较该证书链和预先存储在应用中的信息。 如果比较结果表明它与应用中存储的信息一致,则可以允许通信进行;否则,应该中止通信过程。 但是,如果应用使用下面列出的方法,尝试获取在握手期间受系统信任的证书链,则应用可能无法获得预期的证书链,从而存在固定可能无法正常工作的风险 [26]。
[26] 这篇文章详细解释了风险:https://www.cigital.com/blog/ineffective-certificate-pinning-implementations/。
javax.net.ssl.SSLSession.getPeerCertificates()
javax.net.ssl.SSLSession.getPeerCertificateChain()
这些方法返回的东西,不是在握手过程中受系统信任的证书链,而是应用从通信伙伴本身接收到的证书链。 因此,即使中间人攻击导致证书链中附加不正当证书机构的证书,上述方法也不会返回握手期间受系统信任的证书; 相反,应用最初试图连接的服务器的证书也将同时返回。 由于固定,这个证书将等同于预先存储在应用中的证书;因此重新测试它不会检测到任何不当行为。 由于这个以及其他类似的原因,在握手后执行重新测试时,最好避免使用上述方法。
在 Android 版本4.2(API 级别 17)及更高版本中,使用net.http.X509TrustManagerExtensions中的checkServerTrusted()
方法,将允许应用仅获取握手期间受系统信任的证书链。
一个示例,展示了使用X509TrustManagerExtensions
的固定
// Store the SHA-256 hash value of the public key included in the correct certificate for the remote server (pinning)
private static final Set<String> PINS = new HashSet<>(Arrays.asList(
new String[] {
"d9b1a68fceaa460ac492fb8452ce13bd8c78c6013f989b76f186b1cbba1315c1",
"cd13bb83c426551c67fabcff38d4496e094d50a20c7c15e886c151deb8531cdc"
}
));
// Communicate using AsyncTask work threads
protected Object doInBackground(String... strings) {
[...]
// Obtain the certificate chain that was trusted by the system by testing during the handshake
X509Certificate[] chain = (X509Certificate[]) connection.getServerCertificates();
X509TrustManagerExtensions trustManagerExt = new X509TrustManagerExtensions((X509TrustManager) (trus
tManagerFactory.getTrustManagers()[0]));
List<X509Certificate> trustedChain = trustManagerExt.checkServerTrusted(chain, "RSA", url.getHost());
// Use public-key pinning to test
boolean isValidChain = false;
for (X509Certificate cert : trustedChain) {
PublicKey key = cert.getPublicKey();
MessageDigest md = MessageDigest.getInstance("SHA-256");
String keyHash = bytesToHex(md.digest(key.getEncoded()));
// Compare to the hash value stored by pinning
if(PINS.contains(keyHash)) isValidChain = true;
}
if (isValidChain) {
// Proceed with operation
} else {
// Do not proceed with operation
}
[...]
}
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String s = String.format("%02x", b);
sb.append(s);
}
return sb.toString();
}
Google Play 服务(版本 5.0 和更高)提供了一个称为 Provider Installer 的框架。 这可以用于解决安全供应器中的漏洞,它是 OpenSSL 和其他加密相关技术的实现。 详细信息请参见 “5.6.3.5 通过 Google Play 服务解决安全供应器的漏洞”。
Android 7.0(API Level 24)引入了一个称为“网络安全配置”的框架,允许各个应用为网络通信配置它们自己的安全设置。 通过使用此框架,应用可以轻松集成各种技术,来提高应用安全性,不仅包括与私钥证书和公钥固定的 HTTPS 通信,还可防止未加密(HTTP)通信,以及仅在调试过程中启用的私钥证书 [27]。
[27] 网络安全配置的更多信息,请见 https://developer.android.com/training/articles/security-config.html。
只需通过配置xml
文件中的设置,即可访问网络安全配置提供的各种功能,它们可应用于整个应用的 HTTP 和 HTTPS 通信。 这消除了修改应用代码或执行任何额外操作的需要,简化了实现并提供了防范组合错误或漏洞的有效方法。
使用私有证书通过 HTTPS 进行通信
“5.4.1.3 通过 HTTPS 与有证书进行通信”部分介绍了与私有证书(例如自签名证书或公司内部证书)的 HTTPS 通信的示例代码。 但是,通过使用网络安全配置,开发人员可以在“5.4.1.2 通过 HTTPS 进行通信”的示例代码中使用私有证书,而无需实现。
使用私有证书与特定域进行通信
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">jssec.org</domain>
<trust-anchors>
<certificates src="@raw/private_ca" />
</trust-anchors>
</domain-config>
</network-security-config>
在上面的示例中,用于通信的私有证书(private_ca
)可以作为资源存储在应用中,带有使用条件及其在.xml
文件中描述的适用范围。 通过使用<domain-config>
标签,私有证书可以仅仅应用于特定域。 为了对应用执行的所有 HTTPS 通信使用私有证书,请使用<base-config>
标签,如下所示。
对应用执行的所有 HTTPS 通信使用私人证书
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="@raw/private_ca" />
</trust-anchors>
</base-config>
</network-security-config>
固定
我们在“5.4.3.5 针对固定的注解和实现示例”中提到了公钥固定。通过使用网络安全配置,如下例所示,你不必在代码中实现认证过程; 相反,xml
文件中的规范足以确保正确的认证。
对 HTTPS 通信使用公钥固定
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">jssec.org</domain>
<pin-set expiration="2018-12-31">
<pin digest="SHA-256">e30Lky+iWK21yHSls5DJoRzNikOdvQUOGXvurPidc2E=</pin>
<!-- 用于备份 -->
<pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>
</pin-set>
</domain-config>
</network-security-config>
上面<pin>
标签描述的数量,是用于固定的公钥的 base64 编码哈希值。 唯一支持的散列函数是 SHA-256。
防止未加密(HTTP)通信
使用网络安全配置可以阻止应用进行 HTTP 通信(未加密通信)。
防止未加密(HTTP)通信
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">jssec.org</domain>
</domain-config>
</network-security-config>
在上面的例子中,我们在<domain-config>
标签中指定了属性cleartextTrafficPermitted="false"
。 这可以防止指定域的 HTTP 通信,从而强制使用 HTTPS 通信。 在<base-config>
标记中包含此属性设置,将会阻止所有域的 HTTP 通信 [28]。但请谨慎注意,此设置不适用于WebView
。
[28] 网络安全配置如何为非 HTTP 连接工作,请参阅以下 API 参考。
特地用于调试目的的私有证书
为了在应用开发过程中进行调试,开发人员可能希望使用私有证书,与某些 HTTPS 服务器进行通信,它们由于应用开发目的而存在。 在这种情况下,开发人员必须注意确保没有危险的实现(包括禁用证书认证的代码)被合并到应用中;这在“5.4.3.3 禁用证书验证的危险代码”一节中讨论。 在网络安全配置中,可以按照下面的示例来配置,来规定一组仅在调试时才使用的证书(仅当AndroidManifest.xml
文件中的android:debuggable
设置为true
时)。 这消除了危险代码可能无意中保留在应用的发行版中的风险,因此是防止漏洞的有效手段。
仅在调试时使用私有证书
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<debug-overrides>
<trust-anchors>
<certificates src="@raw/private_cas" />
</trust-anchors>
</debug-overrides>
</network-security-config>