使用 HTTPS 建议先阅读 Android 官方 Training: Security with SSL。很多公司已经全站 HTTPS,但有些用法并不正确。这里简单记录一下自己遇到的问题。

需要提前了解的知识:

  • 对称加密
  • 非对称加密

证书格式

扩展名说明
.DER二进制格式证书,扩展名通常为 .cer.crt
.PEMBase64 编码的 X.509v3 证书,以 ---BEGIN... 开头
.CRT / .CER基本一致;.CRT 更符合微软标准
.key使用 PKCS #8 算法处理的公钥/私钥文件

使用 OpenSSL 生成自签名证书

生成私钥

1
2
3
4
5
$ openssl genrsa -out key.pem 1024
Generating RSA private key, 1024 bit long modulus
....................++++++
.....................++++++
e is 65537 (0x10001)

创建证书签名请求(CSR)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ openssl req -new -key key.pem -out request.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Beijing
Locality Name (eg, city) []:Beijing
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Hxq
Common Name (e.g. server FQDN or YOUR name) []:hao
Email Address []:haoxiqiang@live.com

Common Name 必须与服务器域名匹配,这是 SSL-RFC 的要求。

生成自签名证书

1
$ openssl x509 -req -days 30 -in request.pem -signkey key.pem -out certificate.pem

从已有服务器获取证书

1
echo | openssl s_client -connect hostname:443 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > hostname.pem

Android 中使用证书

Android 通常只能识别 BKS(Bouncy Castle KeyStore) 类型的证书,需要转换。

转换 PEM 到 BKS

1
2
3
4
5
6
7
8
keytool -importcert -v \
 -trustcacerts \
 -alias 0 \
 -file <(openssl x509 -in hostname.pem) \
 -keystore $CERTSTORE -storetype BKS \
 -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
 -providerpath bcprov-jdk16-1.46.jar \
 -storepass password

相关工具:

在代码中加载 BKS 证书

1
2
3
4
5
6
7
InputStream inputStream = context.getResources().openRawResource(res);
KeyStore keyStore = KeyStore.getInstance("BKS");
keyStore.load(inputStream, password.toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());

如果不想用资源文件,也可以用 String -> InputStream 的方式加载文本形式的证书。

证书格式转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# PEM to DER
$ openssl x509 -outform der -in certificate.pem -out certificate.der
# PEM to P7B
$ openssl crl2pkcs7 -nocrl -certfile certificate.cer -out certificate.p7b -certfile CAcert.cer
# PEM to PFX
$ openssl pkcs12 -export -out certificate.pfx -inkey privateKey.key -in certificate.crt -certfile CAcert.crt
# DER to PEM
$ openssl x509 -inform der -in certificate.cer -out certificate.pem
# P7B to PEM
$ openssl pkcs7 -print_certs -in certificate.p7b -out certificate.cer
# PFX to PEM
$ openssl pkcs12 -in certificate.pfx -out certificate.cer -nodes

查看证书信息

1
2
3
openssl x509 -in cert.pem -text -noout
openssl x509 -in cert.cer -text -noout
openssl x509 -in cert.crt -text -noout

常见问题

SSLPeerUnverifiedException: Hostname not verified

自签名证书的 Common Name 与服务器域名不匹配会触发此异常。可以通过自定义 HostnameVerifier 解决,但注意不要将 verify() 改为始终返回 true

1
2
3
4
5
6
7
8
HostnameVerifier hostnameVerifier = new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        HostnameVerifier hv =
            HttpsURLConnection.getDefaultHostnameVerifier();
        return hv.verify("example.com", session);
    }
};

参考:Hostname Not Verified 问题

References