SSL单向/双向认证详解
1、单向认证和双向认证
单向认证 SSL 协议的具体过程
①客户端的浏览器向服务器传送客户端 SSL 协议的版本号,加密算法的种类,产生的随机数,以及其他服务器和客户端之间通讯所需要的各种信息。
②服务器向客户端传送 SSL 协议的版本号,加密算法的种类,随机数以及其他相关信息,同时服务器还将向客户端传送自己的证书。
③客户利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过期,发行服务器证书的 CA 是否可靠,发行者证书的公钥能否正确解开服务器证书的“发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配。如果合法性验证没有通过, 通讯将断开;如果合法性验证通过,将继续进行第四步。
④用户端随机产生一个用于后面通讯的“对称密码”,然后用服务器的公钥(服务器的公钥从步骤②中的服务器的证书中获得)对其加密,然后将加密后的“预主密码”传给服务器。
⑤如果服务器要求客户的身份认证(在握手过程中为可选),用户可以建立一个随机数然后对其进行数据签名,将这个含有签名的随机数和客户自己的证书以及加密过的“预主密码”一起传给服务器。
⑥ 如果服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性,具体的合法性验证过程包括:客户的证书使用日期是否有效,为客户提供证书的 CA 是否可靠,发行CA 的公钥能否正确解开客户证书的发行 CA 的数字签名,检查客户的证书是否在证书废止列表(CRL)中。检验如果没有通过,通讯立刻中断;如果验证通过,服务器将用自己的私钥解开加密的“预主密码 ”,然后执行一系列步骤来产生主通讯密码(客户端也将通过同样的方法产生相同的主通讯密码)。
⑦服务器和客户端用相同的主密码即“通话密码”,一个对称密钥用于 SSL 协议的安全数据通讯的加解密通讯。同时在 SSL 通讯过程中还要完成数据通讯的完整性,防止数据通讯中的任何变化。
⑧客户端向服务器端发出信息,指明后面的数据通讯将使用的步骤⑦中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。
⑨服务器向客户端发出信息,指明后面的数据通讯将使用的步骤⑦中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。
⑩SSL 的握手部分结束,SSL 安全通道的数据通讯开始,客户和服务器开始使用相同的对称密钥进行数据通讯,同时进行通讯完整性的检验。
双向认证 SSL 协议的具体过程
① 浏览器发送一个连接请求给安全服务器。
② 服务器将自己的证书,以及同证书相关的信息发送给客户浏览器。
③ 客户浏览器检查服务器送过来的证书是否是由自己信赖的 CA 中心所签发的。如果是,就继续执行协议;如果不是,客户浏览器就给客户一个警告消息:警告客户这个证书不是可以信赖的,询问客户是否需要继续。
④ 接着客户浏览器比较证书里的消息,例如域名和公钥,与服务器刚刚发送的相关消息是否一致,如果是一致的,客户浏览器认可这个服务器的合法身份。
⑤ 服务器要求客户发送客户自己的证书。收到后,服务器验证客户的证书,如果没有通过验证,拒绝连接;如果通过验证,服务器获得用户的公钥。
⑥ 客户浏览器告诉服务器自己所能够支持的通讯对称密码方案。
⑦ 服务器从客户发送过来的密码方案中,选择一种加密程度最高的密码方案,用客户的公钥加过密后通知浏览器。
⑧ 浏览器针对这个密码方案,选择一个通话密钥,接着用服务器的公钥加过密后发送给服务器。
⑨ 服务器接收到浏览器送过来的消息,用自己的私钥解密,获得通话密钥。
⑩ 服务器、浏览器接下来的通讯都是用对称密码方案,对称密钥是加过密的。
上面所述的是双向认证 SSL 协议的具体通讯过程,这种情况要求服务器和用户双方都有证书。单向认证 SSL 协议不需要客户拥有 CA 证书,具体的过程相对于上面的步骤,只需将服务器端验证客户证书的过程去掉,以及在协商对称密码方案,对称通话密钥时,服务器发送给客户的是没有加过密的 (这并不影响 SSL 过程的安全性)密码方案。这样,双方具体的通讯内容,就是加过密的数据,如果有第三方攻击,获得的只是加密的数据,第三方要获得有用的信息,就需要对加密 的数据进行解密,这时候的安全就依赖于密码方案的安全。而幸运的是,目前所用的密码方案,只要通讯密钥长度足够的长,就足够的安全。这也是我们强调要求使 用 128 位加密通讯的原因。
2、证书扩展名
在开发和测试阶段,使用的是自签的证书。在找自签CA证书操作流程的时候,会有很多的文件扩展名,如pem,der,csr.cer.crt.p12,pfx,jks等,刚开始接触的话很容易混淆,因此先来看下证书的扩展名。
DER: .DER = DER扩展用于二进制DER编码证书。这些文件也可能承载CER或CRT扩展。 PEM:使⽤Base64 ASCII进⾏编码的纯⽂本格式,是以“ - BEGIN …”前缀的ASCII(Base64)数据。 KEY:.KEY 扩展名用于公钥和私钥,常见使用于私钥。也可以被编码为二进制DER或ASCII PEM。 CSR:证书签名请求。CSR文件是申请SSL证书时所需要的一个数据文件。 CRT:CRT扩展用于证书。 证书可以被编码为二进制DER或ASCII PEM。 CER和CRT扩展几乎是同义词。 最常见的于Unix 或类Unix系统。通俗来讲,.CRT文件常在Linux系统使用,包含公钥和主体信息。 CER:.CRT的替代形式,您可以在微软系统环境下将.CRT转换为.CER(.both DER编码的.CER,或base64 [PEM]编码的.cer)。通俗来讲,就是.CER扩展文件是DER编码,并且.CER文件常在Windows系统使用。 P12:P12证书全称是PKCS#12。是一种交换数字证书的加密标准,用来描述个人身份信息。p12证书包含了私钥、公钥并且有口令保护,在证书泄露后还有最后一道保障——证书口令,不知道正确的证书口令无法提取秘钥(文件的扩展名能够为pfx或p12) PFX:PFX也是由PKCS#12标准定义,包含了公钥和私钥的二进制格式的证书形式,以pfx做为证书文件后缀名(文件的扩展名能够为pfx或p12) JKS:JKS是JAVA的keytools证书工具支持的证书私钥格式
3、实战
openssl准备环境
使用openssl version查看openssl版本,如果没有安转openssl,可以执行 yum install openssl 安装
nginx环境准备
我们使用nginx来进行https的双向认证,首先我们需要安装nginx并附带SSL模块,在linux下安装nginx,首先需要安装 gcc-c++编译器。然后安装nginx依赖的pcre和zlib包。最后安装nginx即可。
1.先安装gcc-c++编译器
yum install gcc-c++
yum install -y openssl openssl-devel
2.再安装pcre包
yum install -y pcre pcre-devel
3.再安装zlib包
yum install -y zlib zlib-deve
4.nginx安装
#在/usr/local/下创建文件nginx文件
mkdir /usr/local/nginx
#在网上下nginx包上传至Linux(https://nginx.org/download/),也可以直接下载
wget https://nginx.org/download/nginx-1.19.9.tar.gz
#解压并进入nginx目录
tar -zxvf nginx-1.19.9.tar.gz cd nginx-1.19.9
#使用nginx默认配置
./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-pcre
#编译安装
make make install
#查找安装路径
whereis nginx
#进入sbin目录,可以看到有一个可执行文件nginx,直接./nginx执行就OK了。
./nginx
#查看是否启动成功
ps -ef | grep nginx
#然后在网页上访问自己的IP就可以了默认端口为80(出现如下欢迎界面就成功了!)
自签CA证书
1、生成根证书
#创建证书存放的⽬录
mkdir ssl
#进⼊证书存放⽬录
cd ssl
#生成私钥
openssl genrsa -out ca.key 2048 #⽣成根证书私钥
#查看一下ca.key 的内容,可以看到
#可以将ca.key转为pem文件
openssl rsa -in ca.key -out ca-key.pem
再查看一下ca-key.pem文件,内容和ca.key是相同的
#生成根证书
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt
-days设置证书有效时间,这里我们设置10年
生成证书时要输入一些个体信息
证书生成后,可以查看ca.crt的内容
#也可以将ca.crt转为pem文件
openssl x509 -in ca.crt -out ca-crt.pem
再查看一下ca-crt.pem文件,内容和ca.crt是相同的
#编码类型转换
PEM编码转为DER编码
openssl x509 -in ca-crt.pem -outform der -out ca-crt.der
DER转为PEM
openssl x509 -in ca-crt.der -inform der -outform pem -out ca-crt.pem
(提示:要转换KEY文件也类似)
der二进制编码的文件不能直接查看,可以使用命令查看.der文件
openssl x509 -in ca-crt.der -inform der -text -noout
2、生成服务端证书
为了便于理解和操作,这里统一使用pem编码,并统一生成pem格式扩展文件
#生成服务端私钥
# 生成pem扩展名的私钥
openssl genrsa -out server-key.pem 2048
# 2048表示生成的私钥为2048位,一般使用2048位相对比较安全
#将.pem扩展名私钥转为.key
openssl rsa -in server-key.pem -out server.key
再查看server.key 和server.pem 私钥内容是相同的
#生成服务端证书签名请求文件
#注意这⾥的common name必须是需要访问的域名,其他的内容可以和根证书填写的⼀样
openssl req -new -key server-key.pem -out server-csr.pem
#可以将server-csr.pem转为.csr扩展名
openssl req -in server-csr.pem -out server.csr 或直接修改扩展名 .pem为.csr
#根据签发请求⽣成服务端证书
openssl x509 -req -sha256 -in server-csr.pem -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -out server-crt.pem
#可以将server-crt.pem转为.crt扩展名
openssl x509 -outform pem -in server-crt.pem -out server.crt
#生成服务端pfx或p12证书
openssl pkcs12 -export -in server.crt -inkey server.key -out server.pfx
注:在将.pem扩展文件转为key,csr,crt文件的时候,先查看文件内容 包含 -----BEGIN RSA PRIVETE KEY ----- 的内容可以导出转换.key扩展名的文件 包含 -----BEGIN CERTIFICATE REQUEST ----- 的内容可以导出转换.csr扩展名的文件 包含 -----BEGIN CERTIFICATE ----- 的内容可以导出转换.crt扩展名的文件
3、生成客户端证书
客户端证书⽣成步骤和服务端基本⼀样,需要注意的就是在⽣成签发请求的时候填写的信息中,comm name也要是访问的域名。
#生成客户端私钥
openssl genrsa -out client.key 2048
#生成客户端证书签名请求文件
openssl req -new -key client.key -out client.csr
#根据签发请求和⽣成客户端证书
openssl x509 -req -sha256 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -out client.crt
#生成客户端pfx或p12证书
openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12
4、开启https,并校验客户端(双向认证)
#配置nginx,开启https
开启https请求
进入nginx目录,编辑nginx.conf – vim nginx.conf
找到HTTPS server
ssl_certificate 服务端crt证书路径
ssl_certificatie_key 服务端私钥路径
配置完以后 启动或者容器一下nginx 启动:在nginx目录执行 ./sbin/nginx 重启:在nginx目录执行 ./sbin/nginx -s reload 在浏览器访问是成功的,因为我们是自签证书,因此显示不安全
#开启客户端认证
vim nginx.conf #继续编辑nginx.conf
ssl_client_certificate #指定客户端认证时使⽤的根证书路径,⽤来验证客户端证书的正确性,我们使用的自签ca证书签发的客户端证书,因此使用ca.crt
ssl_verify_client on #为开启客户端校验
配置完成后重启
nginx ./sbin/nginx -s reload
为了方便测试。我们直接使用curl 命令进行测试
curl https://ip -k -v
ip为访问的具体ip地址
-k编码忽略服务端证书的校验,因为我们这里服务端证书也是自签的,所以要加上-k
不加-k,会有异常提示
-v为显示具体的信息,也可以不加
使用上述命令访问后
提示需要携带客户端的证书,说明我们配置的客户端认证已经生效了
curl --cert client.crt --key client.key https://ip -k
可以看到携带证书后访问是成功的,说明客户端已经认证成功了,因此https双向认证完成了
5.java代码
附一段java代码,代码来自 https://wenku.baidu.com/view/ee8e04315c0e7cd184254b35eefdc8d376ee1494.html
public class SSLService { // 客户端证书路径,⽤了本地绝对路径,需要修改 private final static String CLIENT_CERT_FILE = "C:\\Users\\tzx\\Desktop\\client.p12"; // 客户端证书密码 private final static String CLIENT_PWD = "131112"; // 信任库路径,即keytool⽣成的那个⾃定义名称的库⽂件 private final static String TRUST_STRORE_FILE = "D:\\Java\\jdk1.8.0_131\\jre\\lib\\security\\test.truststore"; // 信任库密码,即keytool时的密码 private final static String TRUST_STORE_PWD = "131112"; private static String readResponseBody(InputStream inputStream) throws IOException { try { BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); StringBuffer sb = new StringBuffer(); String buff = null; while ((buff = br.readLine()) != null) { sb.append(buff + "\n"); } return sb.toString(); } finally { inputStream.close(); } } public static void httpsCall() throws Exception { // 初始化密钥库 KeyManagerFactory keyManagerFactory = KeyManagerFactory .getInstance("SunX509"); KeyStore keyStore = getKeyStore(CLIENT_CERT_FILE, CLIENT_PWD, "PKCS12"); keyManagerFactory.init(keyStore, CLIENT_PWD.toCharArray()); // 初始化信任库 TrustManagerFactory trustManagerFactory = TrustManagerFactory .getInstance("SunX509"); KeyStore trustkeyStore = getKeyStore(TRUST_STRORE_FILE, TRUST_STORE_PWD, "JKS"); trustManagerFactory.init(trustkeyStore); // 初始化SSL上下⽂ SSLContext ctx = SSLContext.getInstance("SSL"); ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory .getTrustManagers(), null); SSLSocketFactory sf = ctx.getSocketFactory(); HttpsURLConnection.setDefaultSSLSocketFactory(sf); String url = "https://blog.tzx.com"; URL urlObj = new URL(url); HttpsURLConnection con = (HttpsURLConnection) urlObj.openConnection(); con.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"); con.setRequestProperty("Accept-Language", "zh-CN;en-US,en;q=0.5"); con.setRequestMethod("GET"); String res = readResponseBody(con.getInputStream()); System.out.println(res); } /** * 获得KeyStore */ private static KeyStore getKeyStore(String keyStorePath, String password, String type) throws Exception { FileInputStream is = new FileInputStream(keyStorePath); KeyStore ks = KeyStore.getInstance(type); ks.load(is, password.toCharArray()); is.close(); return ks; } public static void main(String[] args) throws Exception { httpsCall(null); } }