|
6 K2 s6 ?! p8 z0 f 最近在忙App安全,而双向验证是第一步,本文主要记录我在双向验证过程中遇到的坑,以及解决虽然网上类似的教程贼多,但大部分基于 Tomcat… 小打小闹尚可,毕竟反代还是 Nginx 好通过本文,你将顺利获知如何实现 android 与 nginx 的双向验证。
2 s7 E8 R% r) F/ J! {" y 我们以一段 Nginx 的配置开始吧:server {listen 443 ssl;server_name www.kpromise.top;ssl_certificate /etc/letsencrypt/live/kpromise.top/fullchain.pem; - b9 N* Z8 k; K0 c+ T& ?3 @
ssl_certificate_key /etc/letsencrypt/live/kpromise.top/privkey.pem;... // 此处省略 N 多配置这里,域名是本站域名,另外配置了证书和key,这是普通的 https 配置了。
4 v; B/ K' f* @9 j 接下来,我们看另外一个配置:server {listen 443 ssl;server_name api.kpromise.top;ssl_certificate /root/ssl/server.crt; + h# A- E U0 ^. v6 Q: U* a$ A
ssl_certificate_key /root/ssl/server.key;ssl_password_file /root/ssl/password_file;ssl_client_certificate /root/ssl/ca.crt;
8 w/ j& F# }6 s ssl_verify_client optional_no_ca;location / {root /root/ssl/api/;}}这是一段中规中矩的服务端验证客户端的配置,主要变化是多了 ssl_client_certificate 这行。 6 F. t- L0 S3 t; s- s- B8 i
具体请看 http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_client_certificate那么,这里的ca.crt 又是如何产生的呢,下面进入主题,生成证书:
$ M+ r8 B4 }. N 首先,生成 ca.key 接下来签名证书的时候会用到:1、openssl genrsa -des3 -out ca.key 4096接着,生成 ca.crt 文件,crt 文件是客户端认证的证书文件,同样的,在你签名证书的时候会用到 ( R& A- c: }+ {' }, f
2、openssl req -new -x509 -days 365 -key ca.key -out ca.crt3、生成证书:openssl genrsa -des3 -out server.key 1024
. t3 ?" B/ }: b5 N1 n openssl req -new -key server.key -out server.csr会提示你输入 Common Name 一般是你的域名哦4、用 ca.key 和 ca.crt 签名证书:openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt ) k" ~! m6 v, {- F1 h
5、重复 3-4 两步,但是 -out 改为 client.x 来生成并签署另一份证书,比如:openssl genrsa -des3 -out client.key 1024openssl req -new -key client.key -out client.csr
+ z( P6 w: G$ U! Z. o0 P' W openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt6、修改 Nginx 配置为 我刚开始提供的第二份配置那样,即:
; M, L5 F2 S- k' H Q server {listen 443 ssl;server_name api.kpromise.top;ssl_certificate /root/ssl/server.crt;ssl_certificate_key /root/ssl/server.key; I4 u# e" F7 U* q8 O+ b
ssl_password_file /root/ssl/password_file;ssl_client_certificate /root/ssl/ca.crt;ssl_verify_client on;
3 j5 ~% L0 ^% ]$ C location / {root /root/ssl/api/;}}然后 把相关文件移入配置所列的目录,并执行 nginx -s reload,此时,服务端就会校验客户端证书了这里的 ssl_password_file 对应的是一个普通文件,里面是 创建证书时设置的密码哦。 4 K* {1 s6 R, L. e3 H5 B N
接下来,我们看下 android 端的修改首先查看 android 支持 的 keystore 类型: https://developer.android.com/reference/java/security/KeyStore#summary 大致如下:。 , C* c/ j: o3 a% f. ~2 T
AndroidCAStore 14+AndroidKeyStore 18+BCPKCS12 1-8BKS 1+BouncyCastle 1+PKCS12 1+PKCS12-DEF 1-8可以看得出,AndroidCAStore、PKCS12、BouncyCastle、BKS 都是不错的选择,但是我看网上资料,基本都说 转为 android 支持的类型 BKS 这又是什么鬼?明明有 PKCS12 可以选择啊。
8 B% ~5 h1 \- I( [- X3 Q; \* d% h 我们接下来导出 client.p12 以及 server.p12 文件,命令如下:openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name "client"
( g# t+ {. {( _( L1 r, A openssl pkcs12 -export -in server.crt -inkey server.key -out server.p12 -name "client"然后把 client.p12 和 server.p12 都给android 客户端,android 客户端将他们放到 /src/main/assets/ 下面。
5 ]7 F* K- F7 o3 A* G4 a) b3 ~ 最后,我们修改 okhttp 吧:class SSLHelper {private var clientPw: String? = nullprivate var clientPKCSFileName: String = "client.p12" ) v' \6 y+ n- h$ ?- q
private var serverPKCSFileName: String = "server.p12"private var serverPw: String? = nullprivate val protocolType = "TLS"
9 l, f( Q' d4 F# R0 G( K6 C private val keyStoreType: String = "PKCS12"private val certificateFormat: String = "X509"fun initClient
# n7 `( i W# X1 p7 u& V( S (clientPKCSFileName: String, clientPw: String): SSLHelper {this.clientPKCSFileName = clientPKCSFileName ) Q+ _8 t! k& Q8 a8 b
this.clientPw = clientPwreturn this}fun initServer(serverPKCSFileName: String, serverPw: String): SSLHelper { ; {- P' N* W+ E R! V6 s
this.serverPKCSFileName = serverPKCSFileNamethis.serverPw = serverPwreturn this}fun getSSLCertification
]! V8 q: q! R( c& k) o (context: Context): SSLSocketFactory? {if (clientPw == null) throw RuntimeException("please call initClient first...")
" o: X- R1 o+ c7 j) o if (serverPw == null) throw RuntimeException("please call initServer first...")var sslSocketFactory: SSLSocketFactory? = null
\, M1 j; i2 `3 m/ v- K try {val clientKeyStore = KeyStore.getInstance(keyStoreType)val serverKeyStore = KeyStore.getInstance * J# L6 Y j* @( r3 q
(keyStoreType)val clientPrivateKeyInputStream = context.assets.open(clientPKCSFileName)val serverPublicKeyInputStream = context.assets.
5 w6 X4 ~9 u, z7 T' n$ z" v open(serverPKCSFileName)clientKeyStore.load(clientPrivateKeyInputStream, clientPw?.toCharArray())serverKeyStore.
( T7 x+ f5 C* K \ load(serverPublicKeyInputStream, serverPw?.toCharArray())clientPrivateKeyInputStream.close()serverPublicKeyInputStream. 2 l' U& M* J4 s- B9 k d
close()val sslContext = SSLContext.getInstance(protocolType)val trustManagerFactory = TrustManagerFactory. $ y& g% O; K; N: D; q) Y
getInstance(certificateFormat)val keyManagerFactory = KeyManagerFactory.getInstance(certificateFormat) - `' Z; N# I; `1 \: m6 y' e: p
trustManagerFactory.init(serverKeyStore)keyManagerFactory.init(clientKeyStore, clientPw?.toCharArray()) : G7 Q2 h. U5 Y( [
sslContext.init(keyManagerFactory.keyManagers,trustManagerFactory.trustManagers, null)sslSocketFactory = sslContext.socketFactory ) V4 T! Z5 _- p/ D7 ?5 b
} catch (e: Exception) {e.printStackTrace()}return sslSocketFactory}}fun getBuilder(showLog: Boolean): OkHttpClient.Builder {
2 G" b5 |/ u: I val builder = OkHttpClient.Builder()val sslSocketFactory = this.sslSocketFactoryif (sslSocketFactory != null) { 2 o/ j! `& A- o$ r) x0 L( o! [
builder.sslSocketFactory(sslSocketFactory)}builder.hostnameVerifier { _, _ ->true}... // 此处省略 N 多 代码完美了,当然,hostnameVerifier 你也可以指定自己的域名啊。 % j! B( W+ C7 N' z7 x
遇到的天坑:在生成 ca server 以及 client 时 会 要求输入 CN 即:Common Name (e.g. server FQDN or YOUR name) 这行,请注意这三处虽然都是域名,但千万别设一样,否则会出现 400 The SSL certificate error $ _( f: Y' K" p4 }- n
8 h% L0 z# ^% Q( b5 v6 n* T! e3 h+ ^ W; r
: M. m( ?& b8 Q1 v+ n0 g
! @* h2 c# v+ @% t |