使用 HTTPS 建议先阅读 Android 官方 Training: Security with SSL。很多公司已经全站 HTTPS,但有些用法并不正确。这里简单记录一下自己遇到的问题。
需要提前了解的知识:
证书格式#
| 扩展名 | 说明 |
|---|
.DER | 二进制格式证书,扩展名通常为 .cer 或 .crt |
.PEM | Base64 编码的 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#