5.4.3 高级话题

5.4.3.1 如何创建私有证书并配置服务器

在本节中,将介绍如何在 Linux(如 Ubuntu 和 CentOS)中创建私有证书和配置服务器。 私有证书是指私人签发的服务器证书,并由 Cybertrust 和 VeriSign 等可信第三方证书机构签发的服务器证书通知。

创建私有证书机构

首先,你需要创建一私有证书机构来颁发私有证书。 私有证书机构是指私有创建的证书机构以及私有证书。 你可以使用单个私有证书机构颁发多个私有证书。 存储私有证书机构的个人电脑应严格限制为只能由可信的人访问。

为了创建私有证书机构,必须创建两个文件,例如以下 shell 脚本newca.sh和设置文件openssl.cnf,然后执行它们。 在 shell 脚本中,CASTARTCAEND代表证书机构的有效期,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 脚本中,SVSTARTSVEND代表私有证书的有效期,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.pemcert.pem,如下所示。

SSLCertificateFile "/path/to/svcert.pem"
SSLCertificateKeyFile "/path/to/svkey.pem"

5.4.3.2 将私有证书机构的根证书安装到 Android 操作系统的证书商店

在示例代码“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 服务器。

5.4.3.3 禁止证书验证的危险代码

互联网上发现了很多不正确的示例(代码片段),它们允许应用在证书验证错误发生后,通过 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);

5.4.3.4 HTTP 请求头配置的注意事项

如果你希望为 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();
        
        [...]

5.4.3.5 用于固定的注解和实现示例

当应用使用 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();
}

5.4.3.6 使用 Google Play 服务解决 OpenSSL 漏洞的策略

Google Play 服务(版本 5.0 和更高)提供了一个称为 Provider Installer 的框架。 这可以用于解决安全供应器中的漏洞,它是 OpenSSL 和其他加密相关技术的实现。 详细信息请参见 “5.6.3.5 通过 Google Play 服务解决安全供应器的漏洞”。

5.4.3.7 网络安全配置

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>

书籍推荐