看到公司Mqtt相关有用到单项和双向认证,这里记录一下做法。SSL/TLS双向认证的原理,网上有一大堆,但是真正要和源码对应起来,你也许会像我一样,忍不住来一句:WTF!KeyStore、TrustManagerFactory、CertificateFactory这些都是什么鬼东西,一大堆初始化的,莫名其妙!
好了,吐槽完毕。不知道的东西,以后再慢慢了解吧。我虽然大概知道了一些,但是还不是很确定,所以,这篇文章也不介绍具体这些类到底是做啥的,就当记录下,以备后期查阅。
不管你是用OkHttp
、HttpsUrlConnection
,还是Mqtt
配置SSL,你看关键代码,设置的地方就一个:
SSLSocketFactory sslSocketFactory = ....
xxx.sslSocketFactory(sslSocketFactory)
xxx.setSocketFactory(sslSocketFactory)
本质上就是拿到SSLSocketFactory
,然后进行配置。后面关键代码都是生成SSLSocketFactory
。
Mqtt SSL认证这块,我看网上的代码都是需要依赖BouncyCastle
这个Java库,Android HTTPS请求配置的时候貌似没有看到。我司代码也是进行了依赖。
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
单向认证
单向认证指的是客户端认证服务端。这里采用客户端固定写死服务端的中间证书方式进行(把服务端的中中间证书放到assets目录下)。
/**
* 获取单向认证SocketFactory
* @param interMediateCrtFileInputStream 服务器中间证书
*/
public static SSLSocketFactory getSingleSocketFactory(InputStream interMediateCrtFileInputStream) throws Exception {
Security.addProvider(new BouncyCastleProvider());
X509Certificate caCert = null;
BufferedInputStream bis = new BufferedInputStream(interMediateCrtFileInputStream);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
while (bis.available() > 0) {
caCert = (X509Certificate) cf.generateCertificate(bis);
}
KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
caKs.load(null, null);
caKs.setCertificateEntry("ca-certificate", caCert);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(caKs);
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(null, tmf.getTrustManagers(), null);
return sslContext.getSocketFactory();
}
对上面的方法包装一下:
public static SSLSocketFactory getSingleSocketFactory() {
InputStream inputStream = null;
try {
inputStream = Utils.getApp().getAssets().open("interMediate.pem");
return CertFactory.getSingleSocketFactory(inputStream);
} catch (Exception e) {
e.printStackTrace();
} finally {
CloseUtils.closeIO(inputStream);
}
return null;
}
上面SSLContext
获取实例的时候,传入的是TLSv1.2
,不知道会不会有兼容性问题。
双向认证
双向认证是客户端和服务器相互认证。所以,也需要客户端的证书存在。为了保证客户端的唯一性,如此说来是每个客户端都需要一个证书咯。在这里,我们的客户端证书以及客户端私钥是通过服务端下发的。客户端先进行单向认证,然后向服务端索取客户端自己的证书和私钥,拿到后保存在客户端缓存中。后续认证就不需要向服务端要。
下面代码假设你已经拿到了客户端证书和私钥。根据服务端证书、客户端证书和私钥,以及一个password生成双向认证的SSLSocketFactory
。
public static SSLSocketFactory getSocketFactory(InputStream interMediateCaFile, InputStream clientCrtFile, InputStream clientKeyFile, String password) throws Exception {
Security.addProvider(new BouncyCastleProvider());
// load CA certificate
X509Certificate caCert = null;
BufferedInputStream bis = new BufferedInputStream(interMediateCaFile);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
while (bis.available() > 0) {
caCert = (X509Certificate) cf.generateCertificate(bis);
}
// load client certificate
bis = new BufferedInputStream(clientCrtFile);
X509Certificate cert = null;
while (bis.available() > 0) {
cert = (X509Certificate) cf.generateCertificate(bis);
}
// load client private key
PEMParser pemParser = new PEMParser(new InputStreamReader(clientKeyFile));
Object object = pemParser.readObject();
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder()
.build(password.toCharArray());
JcaPEMKeyConverter converter = new JcaPEMKeyConverter()
.setProvider("BC");
KeyPair key;
if (object instanceof PEMEncryptedKeyPair) {
LogUtils.e("Encrypted key - we will use provided password");
key = converter.getKeyPair(((PEMEncryptedKeyPair) object)
.decryptKeyPair(decProv));
} else {
LogUtils.e("Unencrypted key - no password needed");
key = converter.getKeyPair((PEMKeyPair) object);
}
pemParser.close();
// CA certificate is used to authenticate server
KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
caKs.load(null, null);
caKs.setCertificateEntry("ca-certificate", caCert);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(caKs);
// client key and certificates are sent to server so it can authenticate
// us
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("certificate", cert);
ks.setKeyEntry("private-key", key.getPrivate(), password.toCharArray(),
new java.security.cert.Certificate[]{cert});
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory
.getDefaultAlgorithm());
kmf.init(ks, password.toCharArray());
// finally, create SSL socket factory
SSLContext context = SSLContext.getInstance("TLSv1.2");
context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return context.getSocketFactory();
}
使用的话如下,关键证书的获取请参照下就好,password
传入的是空字符串。
public static SSLSocketFactory getSocketFactory() {
InputStream interMediateCrtFile = null;
InputStream crtFile = null;
InputStream keyFile = null;
try {
interMediateCrtFile = Utils.getApp().getAssets().open("interMediate.pem");
crtFile = new FileInputStream(getCertFile());
keyFile = new FileInputStream(getKeyFile());
return CertFactory.getSocketFactory(interMediateCrtFile, crtFile, keyFile, "");
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
CloseUtils.closeIO(interMediateCrtFile, crtFile, keyFile);
}
return null;
}
以上代码不知道是前人从网上哪里抄过来的,搜索后的来源大概是:
网友评论