Android:关于HTTPS、TLSSSL认证以及客户端证书导⼊⽅法⼀、HTTPS 简介
HTTPS 全称 HTTP over TLS/SSL(TLS就是SSL的新版本3.1)。TLS/SSL是在传输层上层的协议,应⽤层的下层,作为⼀个安全层⽽存在,翻译过来⼀般叫做传输层安全协议。对 HTTP ⽽⾔,安全传输层是透明不可见的,应⽤层仅仅当做使⽤普通的 Socket ⼀样使⽤SSLSocket 。TLS是基于 X.509 认证,他假定所有的数字证书都是由⼀个层次化的数字证书认证机构发出,即 CA。另外值得⼀提的是 TLS 是独⽴于 HTTP 的,使⽤了RSA⾮对称加密,对称加密以及HASH算法,任何应⽤层的协议都可以基于 TLS 建⽴安全的传输通道,如 SSH 协议。
代⼊场景:假设现在 A 要与远端的 B 建⽴安全的连接进⾏通信。
1. 直接使⽤对称加密通信,那么密钥⽆法安全的送给 B 。
2. 直接使⽤⾮对称加密,B 使⽤ A 的公钥加密,A 使⽤私钥解密。但是因为B⽆法确保拿到的公钥就是A的公钥,因此也不能防⽌中间⼈
攻击。
为了解决上述问题,引⼊了⼀个第三⽅,也就是上⾯所说的 CA(Certificate Authority):
CA ⽤⾃⼰的私钥签发数字证书,数字证书中包含A的公钥。然后 B 可以⽤ CA 的根证书中的公钥来解密 CA 签发的证书,从⽽拿到A 的公钥。那么⼜引⼊了⼀个问题,如何保证 CA 的公钥是合法的呢?答案就是现代主流的浏览器会内置 CA 的证书。
中间证书:
现在⼤多数CA不直接签署服务器证书,⽽是签署中间CA,然后⽤中间CA来签署服务器证书。这样根证书可以离线存储来确保安全,即使中间证书出了问题,可以⽤根证书重新签署中间证书。另⼀个原因是为了⽀持⼀些很古⽼的浏览器,有些根证书本⾝,也会被另外⼀个很古⽼的根证书签名,这样根据浏览器的版本,可能会看到三层或者是四层的证书链结构,如果能看到四层的证书链结构,则说明浏览器的版本很⽼,只能通过最早的根证书来识别
校验过程
那么实际上,在 HTTPS 握⼿开始后,服务器会把整个证书链发送到客户端,给客户端做校验。校验的过程是要到这样⼀条证书链,链中每个相邻节点,上级的公钥可以校验通过下级的证书,链的根节点是设备信任的锚点或者根节点可以被锚点校验。那么锚点对于浏览器⽽⾔就是内置的根证书啦(注:根节点并不⼀定是根证书)。校验通过后,视情况校验客户端,以及确定加密套件和⽤⾮对称密钥来交换对称密钥。从⽽建⽴了⼀条安全的信道。
⼆、HTTPS API :SSLSocketFactory 或SSLSocket
Android 使⽤的是 Java 的 API。那么 HTTPS 使⽤的 Socket 必然都是通过SSLSocketFactory 创建的 SSLSocket,当然⾃⼰实现了TLS 协议除外。
⼀个典型的使⽤ HTTPS ⽅式如下: (ps:⽹络连接⽅式有HttpClient(5.0开始废弃)、HttpURLConnection、OKHttp 和 Volley)
URL url = new URL("google");
HttpsURLConnection urlConnection = url.openConnection();
InputStream in = InputStream();
此时使⽤的是默认的SSLSocketFactory(没有加载⾃⼰的证书),与下段代码使⽤的SSLContext是⼀致的:
private synchronized SSLSocketFactory getDefaultSSLSocketFactory() {
try {
SSLContext sslContext = Instance("TLS");
sslContext.init(null, null, null);
return defaultSslSocketFactory = SocketFactory();
} catch (GeneralSecurityException e) {
throw new AssertionError(); // The system has no TLS. Just give up.
}
}
默认的 SSLSocketFactory 校验服务器的证书时,会信任设备内置的100多个根证书。
三、SSL的配置
⾃定义信任策略
如果不加载⾃⼰的证书,系统会为你配置好⼀个安全的 SSL,但系统默认的 SSL认为⼀切 CA 都是可
信的,可往往 CA 有时候也不可信,⽐如某家 CA 被⿊客⼊侵什么的事屡见不鲜。虽然 Android 系统⾃⾝可以更新信任的 CA 列表,以防⽌⼀些 CA 的失效,如果为了更⾼的安全性,可以希望指定信任的锚点,类似采⽤如下的代码:
// 取到证书的输⼊流
InputStream caInput = Resources().openRawResource(R.raw.ca_cert);
Certificate ca = Instance("X.509").generateCertificate(caInput);
// 创建 Keystore 包含我们的证书
KeyStore keyStore = DefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// 创建⼀个 TrustManager 仅把 Keystore 中的证书作为信任的锚点
TrustManagerFactory tmf = DefaultAlgorith
m());
tmf.init(keyStore);
// ⽤ TrustManager 初始化⼀个 SSLContext
ssl_ctx = Instance("TLS"); //定义:public static SSLContext ssl_ctx = null;
ssl_ctx.init(null, TrustManagers(), new SecureRandom());
然后可以通过SSLSocketFactory 与服务器进⾏交互:
// SSLSocketFactory 或 SSLSocket 都⾏
//1.创建监听指定服务器地址以及指定服务器监听的端⼝号
SSLSocketFactory socketFactory = (SSLSocketFactory)SocketFactory();
ssl_socket = (SSLSocket) ateSocket(serverUrl, Integer.parseInt(serverPort)); //定义:private final String serverUrl = "42.98.106.44";
// private final String serverPort = "8086"; //2.拿到客户端的socket对象的输出/输⼊流,通过read/write⽅法和服务器交互数据
ssl_input = new BufferedInputStream(InputStream());
ssl_output = new BufferedOutputStream(OutputStream());
以上做法只有我们的 才会作为信任的锚点,只有 以及他签发的证书才会被信任。
说起来有个很有趣的玩法,考虑到证书会过期、升级,我们既不想只信任我们服务器的证书,⼜不想信任 Android 所有的 CA 证书。有个不错的的信任⽅式是把签发我们服务器的证书的根证书导出打包到 APK 中,然后⽤上述的⽅式做信任处理。仔细思考⼀下,这未尝不是⼀种好的⽅式。只要⽇后换证书还⽤这家 CA 签发,既不⽤担⼼失效,安全性⼜有了⼀定的提⾼。因为⽐起信任100多个根证书,只信任⼀个风险会⼩很多。正如最开始所说,信任锚点未必需要根证书。因此同样上⾯的代码也可以⽤于⾃签名证书的信任,相信看官们能举⼀反三,就不再多述。
证书固定
上⽂⾃定义信任锚点的时候说了⼀个很有意思的⽅式,只信任⼀个根CA,其实更加⼀般化和灵活的做
法就是⽤证书固定。
其实 HTTPS 是⽀持证书固定技术的(CertificatePinning),通俗的说就是对证书公钥做校验,看是不是符合期望。HttpsUrlConnection 并没有对外暴露相关的API,⽽在 Android ⼤放光彩的 OkHttp 是⽀持证书固定的,虽然在 Android 中,OkHttp 默认的 SSL 的实现也是调⽤了 Conscrypt,但是重新⽤ TrustManager 对下发的证书构建了证书链,并允许⽤户做证书固定。具体 API 的⽤法可见 CertificatePinner 这个类,这⾥不再赘述。
域名校验
Android 内置的 SSL 的实现是引⼊了Conscrypt 项⽬,⽽ HTTP(S)层则是使⽤的OkHttp。⽽ SSL 层只负责校验证书的真假,对于所有基于SSL 的应⽤层协议,需要⾃⼰来校验证书实体的⾝份,因此 Android 默认的域名校验则由 OkHostnameVerifier 实现的,从HttpsUrlConnection 的代码可见⼀斑:
static {
try {
defaultHostnameVerifier = (HostnameVerifier)
Class.forName("com.android.okhttp.internal.tls.OkHostnameVerifier")
.getField("INSTANCE").get(null);
} catch (Exception e) {
throw new AssertionError("Failed to obtain okhttp HostnameVerifier", e);
}
}
如果校验规则⽐较特殊,可以传⼊⾃定义的校验规则给 HttpsUrlConnection。同样,如果要基于 SSL 实现其他的应⽤层协议,千万别忘了做域名校验以证明证书的⾝份。
四、关于证书
1.证书概念:证书是对现实⽣活中某个⼈或者某件物品的价值体现⽐如古董颁发见证书,⼈颁发献⾎证等通常证书会包含以下内容: 证书拥有者名称(CN),组织单位(OU)组织(O),城市(L)区(ST)国家/地区( C )
证书的过期时间证书的颁发机构证书颁发机构对证书的签名,签名算法,对象的公钥等
数字证书的格式遵循X.509标准。X.509是由国际电信联盟(ITU-T)制定的数字证书标准。
2. 证书类型:
JKS:数字证书库。JKS⾥有KeyEntry和CertEntry,在库⾥的每个Entry都是靠别名(alias)来识别的。
P12:是PKCS12的缩写。同样是⼀个存储私钥的证书库,由.jks⽂件导出的,⽤户在PC平台安装,⽤于标⽰⽤户的⾝份。
CER:俗称数字证书,⽬的就是⽤于存储公钥证书,任何⼈都可以获取这个⽂件。
BKS:由于Android平台不识别.keystore和.jks格式的证书库⽂件,因此Android平台引⼊⼀种的证书库格式,BKS。
下图展⽰了证书的使⽤流程:
为什么Tomcat只有⼀个server.keystore⽂件,⽽客户端需要两个库⽂件?
因为有时客户端可能需要访问多个服务器,⽽服务器的证书都不相同,因此客户端需要制作⼀个truststore来存储受信任的服务器的证书列表。因此为了规范创建⼀个truststore.jks⽤于存储所有受信任
的服务器证书,创建⼀个client.jks来存储客户端⾃⼰的私钥。对于只涉及与⼀个服务端进⾏双向认证的应⽤,将导⼊到client.jks中即可。
导⼊BKS使⽤代码⽰例:(上⾯“SSL的配置”部分已展⽰过导⼊证书的⽅式)
KeyStore keyStore = Instance("BKS"); // 访问keytool创建的Java密钥库
InputStream keyStream = Resources().openRawResource(R.raw.alitrust);
char keyStorePass[]="123456".toCharArray(); //证书密码
keyStore.load(keyStream,keyStorePass);
TrustManagerFactory trustManagerFactory = DefaultAlgorithm());
trustManagerFactory.init(keyStore);//保存服务端的授权证书
ssl_ctx = Instance("SSL");
ssl_ctx.init(null, TrustManagers(), null);
3.制作证书:
⽅式⼀:利⽤keytool⽣成证书
①.⽣成客户端keystore:
keytool -genkeypair -alias client -keyalg RSA -validity 3650 -keypass 123456 -storepass 123456 -keystore client.jks
②.⽣成服务端keystore:
keytool -genkeypair -alias server -keyalg RSA -validity 3650 -keypass 123456 -storepass 123456 -keystore server.keystore
//注意:CN必须与IP地址匹配,否则需要修改host
③.导出客户端证书:
keytool -export -alias client - -keystore client.jks -storepass 123456
④.导出服务端证书:
keytool -export -alias server - -keystore server.keystore -storepass 123456
⑤.证书交换:
将客户端证书导⼊服务端keystore中,再将服务端证书导⼊客户端keystore中,⼀个keystore可以导⼊多个证书,⽣成证书列表。
⽣成客户端信任证书库(由服务端证书⽣成的证书库):
keytool -import -v -alias server - -keystore truststore.jks -storepass 123456
将客户端证书导⼊到服务器证书库(使得服务器信任客户端证书):
keytool -import -v -alias client - -keystore server.keystore -storepass 123456
⑥.⽣成Android识别的BKS库⽂件:
//将client.jks和truststore.jks分别转换成client.bks和truststore.bks,然后放到android客户端的assert⽬录下,
//然后再通过 Assets().open("xxx.bks") 获得⽂件输⼊流;
keytool -importcert -trustcacerts -keystore key.bks -file client.jks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider
keytool -importcert -trustcacerts -keystore key.bks -file truststore.jks -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider
⑦.配置Tomcat服务器:
修改l⽂件,配置8443端⼝
<Connector port="8443" protocol="http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="true" sslProtocol="TLS"
keystoreFile="${catalina.base}/key/server.keystore" keystorePass="123456"
truststoreFile="${catalina.base}/key/server.keystore" truststorePass="123456"/>
备注: - keystoreFile:指定服务器密钥库,可以配置成绝对路径,本例中是在Tomcat⽬录中创建了⼀个
名为key的⽂件夹,仅供参考。 - keystorePass:密钥库⽣成时的密码
- truststoreFile:受信任密钥库,和密钥库相同即可
- truststorePass:受信任密钥库密码
⑧.Android App读取BKS,创建⾃定义的SSLSocketFactory:
private final static String CLIENT_PRI_KEY = "client.bks";
private final static String TRUSTSTORE_PUB_KEY = "truststore.bks";
private final static String CLIENT_BKS_PASSWORD = "123456";
private final static String TRUSTSTORE_BKS_PASSWORD = "123456";
private final static String KEYSTORE_TYPE = "BKS";
private final static String PROTOCOL_TYPE = "TLS";
private final static String CERTIFICATE_FORMAT = "X509";
public static SSLSocketFactory getSSLCertifcation(Context context) {
SSLSocketFactory sslSocketFactory = null;
try {
// 服务器端需要验证的客户端证书,其实就是客户端的keystore
KeyStore keyStore = Instance(KEYSTORE_TYPE);// 客户端信任的服务器端证书
KeyStore trustStore = Instance(KEYSTORE_TYPE);//读取证书
InputStream ksIn = Assets().open(CLIENT_PRI_KEY);//加载客户端私钥
InputStream tsIn = Assets().open(TRUSTSTORE_PUB_KEY);//加载证书
keyStore.load(ksIn, CLIENT_CharArray());
trustStore.load(tsIn, TRUSTSTORE_CharArray());
ksIn.close();
tsIn.close();
//初始化SSLContext
SSLContext sslContext = Instance(PROTOCOL_TYPE);
TrustManagerFactory trustManagerFactory = Instance(CERTIFICATE_FORMAT);
KeyManagerFactory keyManagerFactory = Instance(CERTIFICATE_FORMAT);
trustManagerFactory.init(trustStore);
keyManagerFactory.init(keyStore, CLIENT_CharArray());
sslContext.KeyManagers(), TrustManagers(), null);
sslSocketFactory = SocketFactory();
} catch (KeyStoreException e) {...}//省略各种异常处理,请⾃⾏添加
return sslSocketFactory;
}
⑨Android App通过OkHttpClient进⾏⽹络访问:
//⾃定义⽅法,获取OkHttpClient实例:
public static OkHttpClient getOkHttpClient(SSLSocketFactory sslSocketFactory) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
tTimeout(15L, TimeUnit.SECONDS);
builder.sslSocketFactory(sslSocketFactory ); //添加sslSocketFactory
builder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true; //⾃定义判断逻辑:true-安全,false-不安全
}
});
return builder.build();
}
......
//activity端传⼊之前创建的sslSocketFactory 拿到OkHttpClient 实例后便可进⾏post和get请求:
OkHttpClient okHttpClient = getOkHttpClient(sslSocketFactory);
// 发送格式定义
MediaType JSON
= MediaType.parse("application/json; charset=utf-8");
MediaType STRING
= MediaType.parse("text/x-markdown; charset=utf-8");
// post请求(以json格式发送)=====================================
JSONObject jsonObject = new JSONObject();
jsonObject.put("Model", "KK309");
jsonObject.put("Vid", "0x1234");
jsonObject.put("Pid", "0x5678");
jsonObject.put("Version", 99);
String requestBody = String(1);
final Request postReq = new Request.Builder()
.url(url) //填⼊⾃⼰服务器的URL地址
.
ate(JSON, requestBody))
.build();
Call postCall = wCall(postReq);
@Override
public void onFailure(Call call, IOException e) {
Log.d("SSL", "Post ---> onFailure: "+ e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.d("SSL", "Post ---> onResponse: " + response.body().string());
}
});
// get请求===================================================
final Request getReq = new Request.Builder()
.url(url) //填⼊⾃⼰服务器的URL地址
.get() //默认就是GET请求,可以不写
.build();
Call getCall = wCall(getReq);
@Override
public void onFailure(Call call, IOException e) {
Log.d("SSL", "Get ---> onFailure: "+ e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.d("SSL", "Get ---> onResponse: " + response.body().string());
}
});
⽅式⼆:利⽤openssl⽣成证书(keytool没办法签发证书,⽽openssl能够进⾏签发和证书链的管理)
①创建CA私钥,创建⽬录ca:
openssl genrsa -des3 -out ca/ca-key.pem 1024 //-des:表⽰⽣成的key是有密码保护的
(注:如果是将⽣成的key与server的证书⼀起使⽤,最好不需要密码,就是不要这个参数,不然客户端每次使⽤都需要输⼊密码)
openssl rsa -in ca-key.pem -dpassword.pem //也可以⽤此命令让其不需要输密码
②创建证书请求:
openssl req -new -out ca/ca-req.csr -key ca/ca-key.pem
以下为终端输出信息:
Enter pass phrase for ca/ca-key.pem:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-
----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:ZheJiang
Locality Name (eg, city) []:hz
ssl协议全称Organization Name (eg, company) [Internet Widgits Pty Ltd]:happylife
Organizational Unit Name (eg, section) []:test
Common Name (e.g. server FQDN or YOUR name) []:test1
Email Address []:test2
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:123456
An optional company name []:nanosic
③⾃签署证书:
openssl x509 -req -in ca/ca-req.csr -out ca/ca-cert.pem -signkey ca/ca-key.pem -days 3650
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论