SSL协议:加密、协议消息流、证书。

2023年 10月 7日 43.5k 0

1 预备知识

1 .1 对称加密

对称加密的核心思想:同一个密钥既可以对明文进行加密,又可以对使用该密钥加密的密文进行解密。

假设在公共网络上,Alice需要与Bob进行通信,但是为了防止明文泄露,需要对通信内容进行加密。通信过程如下:

1. Alice和Bob提前协商好共享密钥Key,该密钥不能被第三人知道。

2. Alice使用密钥Key对要发送的明文M进行加密,该过程采用加密算法ALG。

3. 加密后明文M变成密文m。

4. Alice将密文m发送给Bob。

5. Bob收到密文m后使用共享密钥Key对其解密,该过程采用解密算法ALG’。

6. 解密后m恢复成了明文M。

在上述过程中,共享密钥只有Alice和Bob两人知道,即使网络中有攻击者截获了密文m,也不能恢复出M。因此通信内容的机密性得到了保证。但是使用对称加密的通信双方需要有一个共享密钥,该共享密钥的安全性关系到双方之间的通信安全。假设该通信体系内有很多主体都需要跟其他主体进行通信,如果每两个主体直接都维护一个共享密钥,这些密钥的数量会非常庞大。 随着体系中通信主体数量的增加,密钥的数量也随之大幅增加。这使得针对对称加密中使用的密钥进行安全管理和分发变得十分困难且成本高昂。因此只使用对称加密对于保证网络通信安全是有弊端的,而非对称加密算法无需解决密钥分发和管理的问题。

图1-1是对称加密算法的图示。

image.png

图1-1 对称加密示例

图示给出的加密算法ALG只是个示例,实际上的加密算法会复杂很多。常用的对称加密算法有:AES、DES等。(MD5和SHA系列算法不属于加密算法,而是消息摘要算法。因为这些算法使用的hash函数不可逆,也就无法对加密后的数据进行解密)。

1 .2 非对称加密

非对称加密算法是指加密和解密使用不同密钥的加密算法,也叫公钥加密算法。使用非对称加密算法的通信主体都包含一个密钥对(公开密钥和与其配对的私有密钥),该机制下加解密方式有两种:

1)公钥加密,私钥解密。

2)私钥签名,公钥验签。(本质上是私钥加密、公钥解密)

注意:

a 公钥是公开的,私钥是私有的。

公钥可以公开,可以被通信体系内的任何其他主体获取,而私钥只能由产生

该私钥的主体本身保存,私钥一旦泄露,则该主体的通信不再安全。

b 私钥可以推导出公钥,公钥无法推出私钥。****

私钥只能被产生该私钥的主体本身所拥有,公钥可以被其他主体获取。

c 私钥加密的数据可以被公钥解密,而公钥又可以被其他用户获取,这一特点正好被数字签名所利用。

下面举例说明私钥签名和公钥验签的过程。以浏览器访问某可靠网站为例,用户Alice通过浏览器访问某个web服务器Bob时,Bob需要向Alice证明与自己通信是安全的。于是:

1. Bob向CA机构请求数字证书。Bob生成公私钥对,并将自己的身份信息和公钥发送给CA机构,请求颁发数字证书。

2. CA机构颁发数字证书给Bob。CA收到Bob发来的请求,使用摘要算法(也叫指纹算法)将Bob的身份信息和公钥等生成摘要(也叫指纹信息),然后使用CA的私钥对摘要进行加密生成数字签名,然后将Bob的身份信息、公钥信息、摘要以及数字签名等共同组成Bob的数字证书,并将其发送给Bob。

3. Bob将数字证书发送给Alice,以表明自己的身份信息。Bob收到自己的数字证书后,将其发送给Alice验证。

4. Alice接收并验证Bob的数字证书。Alice对Bob使用相同的哈希函数将Bob的数字证书中的明文信息生成摘要,并将其与 使用CA的公钥(一般CA机构本身的数字证书都提前预置在操作系统或浏览器里,该证书中含有CA的公钥信息)对数字签名进行解密得到的摘要 进行对比,如果摘要一致,则说明数据未被纂改,于是信任该证书。

上述为私钥签名、公钥验签的简单案例,其过程如图1-2所示。

image.png

图1-2 私钥签名、公钥验签的过程

1 .3 对称加密和非对称加密的对比

对称加密和非对称加密各有优点和缺点。对称加密的优点是可以快速加密和解密数据,但由于通信双方都必须共享相同的密钥信息,密钥的交换可能是一个问题。对于非对称密钥加密,由于公钥是不需要保密的,因此不存在密钥交换的问题。但是非对称加密算法用于加密和解密数据需要大量的计算,因此非常缓慢。

因此在实际中非对称加密往往和对称加密一起使用。使用非对称加密协商通信双方的会话密钥,然后使用会话密钥对通信内容进行加密。

 

表1-1 对称加密和非对称加密对比

对比 对称加密算法 非对称加密算法
优点 加解密速度快 不存在密钥分发和管理问题
缺点 密钥分发和管理不方便 加解密速度较慢
常用算法 DES、AES RSA、ECC、SM2(国密)

 

 

2 SSL协议

2.1 SSL协议简介

SSL(Secure Sockets Layer安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层对网络连接进行加密。
Secure Socket Layer,为Netscape所研发,用以保障在Internet上数据传输之安全,利用数据加密(Encryption)技术,可确保数据在网络上之传输过程中不会被截取及窃听。一般通用之规格为40 bit之安全标准,美国则已推出128 bit之更高安全标准,但限制出境。只要3.0版本以上之I.E.或Netscape浏览器即可支持SSL。

 
image.png

图2-1  SSL协议高度解耦、可装可卸

 

SSL协议位于TCP/IP协议与各种应用层协议之间,如图2-1所示。为数据通讯提供安全支持。SSL协议可分为两层:SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。SSL协议模型如图2-2所示。

 

image.png

图2-2  SSL协议模型

SSL可以提供的服务:

1)认证性:认证用户和服务器,确保数据发送到正确的客户机和服务器;

2)数据私密性:加密数据以防止数据中途被窃取;

3)数据完整性:维护数据的完整性,确保数据在传输过程中不被改变。

2.2 使用wireshark工具抓包SSL握手协议消息流

如图2-1所示是握手协议的交互过程。

image.png

图2-1 SSL握手协议的流程

使用抓包工具对具体消息内容进行分析,数据来源是阿里云官网首页TLS数据包。

1. ClientHello。这条消息将客户端的功能和首选项传送给服务器。

image.png
Version: 协议版本(protocol version)指示客户端支持的最佳协议版本

Random: 一个32字节数据,28字节是随机生成的,剩余的4字节包含额外的信息,与客户端时钟有关。在握手时,客户端和服务器都会提供随机数,客户端的暂记作 random_C (用于后续的密钥的生成)。这种随机性对每次握手都是独一无二的,在身份验证中起着举足轻重的作用。它可以防止重放攻击,并确认初始数据交换的完整性。

Session ID: 在第一次连接时,会话ID(session ID)字段是空的,这表示客户端并不希望恢复某个已存在的会话。典型的会话ID包含32字节随机生成的数据,一般由服务端生成通过 ServerHello 返回给客户端。

Cipher Suites: 密码套件(cipher suite)块是由客户端支持的所有密码套件组成的列表,该列表是按优先级顺序排列的

Compression: 客户端可以提交一个或多个支持压缩的方法。默认的压缩方法是 null,代表没有压缩

Extensions: 扩展(extension)块由任意数量的扩展组成。这些扩展会携带额外数据

2. ServerHello。将服务器选择的连接参数传回客户端。

image.png

这个消息的结构与 ClientHello 类似,只是每个字段只包含一个选项,其中包含服务端的 random_S 参数 (用于后续的密钥协商)。图中的cipher Suite是服务器最终决定的密钥套件。服务器无需支持客户端支持的最佳版本。如果服务器不支持与客户端相同的版本,可以提供某个其他版本以期待客户端能够接受。

3. Certificate 。典型的Certificate消息用于携带服务器X.509证书链。

image.png

服务器必须保证它发送的证书与选择的算法套件一致。比方说,公钥算法与套件中使用的必须匹配。除此以外,一些密钥交换算法依赖嵌入证书的特定数据,而且要求证书必须以客户端支持的算法签名。所有这些都表明服务器需要配置多个证书(每个证书可能会配备不同的证书链)。

Certificate 消息是可选的,因为并非所有套件都使用身份验证,也并非所有身份验证方法都需要证书。更进一步说,虽然消息默认使用X.509证书,但是也可以携带其他形式的标志;一些套件就依赖PGP密钥

4. ServerKeyExchange 携带密钥交换需要的额外数据。

image.png

ServerKeyExchange 是可选的,消息内容对于不同的协商算法套件会存在差异。从图中可以看出,此次密钥协商使用的DH算法。部分场景下,比如使用 RSA 算法时,服务器不需要发送此消息。ServerKeyExchange 仅在服务器证书消息(也就是上述Certificate消息)不包含足够的数据以允许客户端交换预主密钥(premaster secret)时才由服务器发送。

  • ServerHelloDone。表明服务器已经将所有预计的握手消息发送完毕。在此之后,服务器会等待客户端发送消息。
  •  
    image.png

  • verify certificate
  • 客户端验证证书的合法性,如果验证通过才会进行后续通信,否则根据错误情况不同做出提示和操作,合法性验证内容包括如下:

    证书链的可信性trusted certificate path;

    证书是否吊销revocation,有两类方式-离线CRL与在线OCSP,不同的客户端行为会不同;

    有效期expiry date,证书是否在有效时间范围;

    域名domain,核查证书域名是否与当前的访问域名匹配;

    由PKI体系的内容可知,对端发来的证书签名是CA私钥加密的,接收到证书后,先读取证书中的相关的明文信息,采用相同的散列函数计算得到信息摘要,然后利用对应CA的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性;然后去查询证书的吊销情况等

    7. ClientKeyExchange。携带密钥交换需要的额外数据。

    image.png

    合法性验证通过之后,客户端计算产生随机数字的预主密钥(pre-master),并用证书公钥加密,发送给服务器并携带客户端为密钥交换提供的所有信息。这个消息受协商的密码套件的影响,内容随着不同的协商密码套件而不同。

    此时客户端已经获取全部的计算协商密钥需要的信息: 两个明文随机数 random_C和random_S与自己计算产生的pre-master,然后得到协商密钥(用于之后的消息加密)

    enc_key = PRF(pre_master, "master secret", random_C + random_S)

     

    注意:这里生成密钥为什么要用三个随机数,这里总结了两点主要的观点。

    a:计算机的随机数都是伪随机,三个随机数一起随机性更强,生成的密钥更难破解。(我的疑惑是random_C和random_S都是明文传输的,攻击者很容易获取到,那这两个随机数对生成的密钥强度会有影响吗?)

    b:client不信任server是否是真的随机数,所以client自己提供一个随机数;同时server也不信任client是否有真的随机数,所以server自己也提供一个随机数。(其实跟第一点差不多,我的疑惑在于random_C和 random_S明文传输很容易被攻击者获取,那它们是否随机还重要吗?)

    8. ChangeCipherSpec。用于通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信。

    image.png

    ChangeCipherSpec不属于握手消息,它是另一种协议,只有一条消息,作为它的子协议进行实现。

     

  • Finished (Encrypted Handshake Message)
  • Finished 消息意味着握手已经完成。消息内容将加密,以便双方可以安全地交换验证整个握手完整性所需的数据。

     
    image.png

    这个消息包含verify_data字段(图中体现不出来,实际上是Encrypted Handshake Message中的一部分),它的值是握手过程中所有消息的散列值。这些消息在连接两端都按照各自所见的顺序排列,并以协商得到的主密钥 (enc_key) 计算散列。这个过程是通过一个伪随机函数(pseudorandom function,PRF)来完成的,这个函数可以生成任意数量的伪随机数据。

    两端的计算方法一致,但会使用不同的标签(finished_label):客户端使用 client finished,而服务器则使用 server finished。

    verify_data=PRF(master_secret,finished_label,Hash(handshake_messages))

    因为 Finished 消息是加密的,并且它们的完整性由协商 MAC 算法保证,所以主动网络攻击者不能改变握手消息并对vertify_data的值造假。在 TLS 1.2 版本中,Finished 消息的长度默认是 12 字节(96 位),并且允许密码套件使用更长的长度。在此之前的版本,除了 SSL 3 使用 36 字节的定长消息,其他版本都使用12字节的定长消息。

  • Server解密(服务端内部操作)
  • 服务器用私钥解密加密的Pre-master数据,基于之前交换的两个明文随机数 random_C和random_S,同样计算得到协商密钥:

    enc_key=PRF(Pre_master, "master secret", random_C + random_S);

    同样计算之前所有收发信息的hash值,然后用协商密钥解密客户端发送的 verify_data_C,验证消息正确性;

     

    11. ChangeCipherSpec

    服务端验证通过之后,服务器同样发送change_cipher_spec以告知客户端后续的通信都采用协商的密钥与算法进行加密通信。

    image.png

  • Finished (Encrypted Handshake Message)
  • 服务器也结合所有当前的通信参数信息生成一段数据(verify_data_S)并采用协商密钥session secret(enc_key)与算法加密并发送到客户端。

    image.png

  • 握手结束(客户端内部操作)
  • 客户端计算所有接收信息的hash值,并采用协商密钥解密verify_data_S,验证服务器发送的数据和密钥,验证通过则握手完成;

  • 加密通信
  • 开始使用协商密钥与算法进行加密通信。

     
    SSL握手阶段客户端发送的Client Hello和服务端发送的Server Hello中各有一个随机数。这两个随机数和客户端用服务器公钥加密的Pre-master secret(也是随机数)一起作为协商的会话密钥的参数,用于生成会话密钥。

     

    2.3 SSL其他协议

    2 .3.1 密钥规格变更协议

    TLS的密码规格变更协议是TLS握手协议的一部分,用于密码切换的同步。

    那么为什么这个协议不叫密码规格开始协议,而是叫密码规格变更协议呢?这是因为即便在密码通信开始之后,客户端和服务器也可以通过重新握手来再次改变密码套件。也就是说,客户端和服务器是使用“不加密”这一密码套件进行通信的,因此通信内容是没有进行加密。

    2 .3.2 警告协议

    TLS的警告协议是TLS握手协议的一部分,用于当发生错误时通知通信对象。当握手协议的过程中产生异常,或者发生消息认证码错误,压缩数据无法解压缩等问题时,会使用该协议。

    2 .3.3 应用数据协议

    应用数据协议是TLS握手协议的一部分,用于和通信对象之间传送应用数据。

    当TLS承载HTTP时候,HTTP的请求和响应就会通过TLS的应用数据协议和TLS记录协议来进行传送。

    3 SSL证书

    SSL证书是一种数字证书,用于加密和验证网络连接的安全性。SSL证书是由数字签名机构签署的,以证明网站或服务器是可信的,从而向访问者提供安全的连接。在一个安全的网络连接中,客户端(如浏览器)会验证服务器的SSL证书,以确保其是由可信的签名机构签署的,并且与客户端正在连接的服务器的域名匹配。如果证书无效或与域名不匹配,客户端将会发出警告,以保护用户的数据安全。

    3.1  SSL证书的内容

    一个SSL证书主要由以下几个部分组成:

    公钥 - 用于加密和解密传输的数据。

    证书签名 - 由数字签名机构签署的数字签名,用于验证SSL证书的有效性和身份。

    指纹信息 - 通过摘要算法根据用户身份信息和公钥信息生成的摘要信息

    证书信息 - 包含证书的所有者、颁发机构、有效期等信息。

    以百度网站为例,其SSL证书基本信息如图3-1所示。

    image.png

    图3-1  SSL证书基本信息

     

    另外,需要注意的是,网站服务器上的SSL证书往往是具有层次结构的证书链,如图3-2所示,分别是根CA给自己颁发的根证书、根CA给中间CA颁发的证书、中间CA给百度网站颁发的数字证书。完整的证书验证过程如下:

    1. 用户需要先验证根证书(根CA的公钥一般会预置在浏览器和操作系统中),从根CA证书中得到中间CA证书和其公钥,

    2. 然后使用该公钥验证中间CA证书,并从中间CA证书中得到网站服务器的数字证书和网站服务器的公钥。

    3. 最后使用公钥对数字证书中的签名进行验证。

    image.png

    图3-2  SSL证书链结构

     

    3.2  SSL证书的类型

    SSL证书可以分为以下三种类型。

    1 域名型SSL证书(DV SSL):只验证网站域名所有权的简易型SSL证书,此类证书仅能起到网站机密信息加密的作用,无法向用户证明网站的真实身份。适用于个人网站、小型组织或企业网站、各类加密应用(如数据库和即时通讯协议等)。如图3-3所示为DV型 SSL证书。

    image.png

    图3-3  DV型SSL证书

     

    2 企业型 SSL 证书(OV SSL):需要验证网站所有单位的真实身份的标准型SSL证书,需要购买者提交组织机构资料和单位授权信等在官方注册的凭证,不仅能起到网站机密信息加密的作用,而且能向用户证明网站的真实身份。所以,推荐在所有电子商务网站使用,因为电子商务需要的是在线信任和在线安全。如图3-4所示是OV SSL证书。

    image.png
    图3-4  OV型证书

     

    3 增强型 SSL 证书(EV SSL):同样是基于SSL/TLS安全协议,都是用于网站的身份验证和信息在网上的传输加密,但验证流程更加具体详细,验证步骤更多,证书所绑定的网站就更加的可靠,可信,它跟普通SSL证书的区别也是明显的,证书上面会显示更多的信息,不仅仅是网站所属单位信息,还有公司地址等等;部署证书后,用户打开网站时,浏览器地址栏会显示绿色,在地址栏还会显示网站所属单位的名称,特别适合金融、保险、p2p、电商、网上支付等等行业。(经过验证,谷歌浏览器和Edge浏览器目前已不支持)

    如图3-5所示为 EV型证书。

    image.png

    图3-5  EV型SSL证书

     

    如下图3-6(百度)和图3-7(中国银行)所示为OV证书和EV证书的区别。可以看出EV证书会显示颁发对象所属单位的名称。

    image.png

    图3-6 OV证书只显示证书有效

    image.png
    图3-7 EV证书显示颁发对象

    3.3  SSL证书保证传输安全的过程

    SSL证书是用于保证数据传输安全的重要工具之一。当使用SSL证书进行加密的数据传输时,会发生以下几个步骤:

    客户端发起请求:客户端通过浏览器向服务器发起请求。

    服务器返回证书:服务器返回其SSL证书给客户端,以供其验证身份。

    客户端验证证书:客户端验证证书,以确保其是由可信的数字签名机构签署的,并且与客户端正在连接的服务器的域名匹配。如果证书无效或与域名不匹配,客户端将会发出警告。

    会话密钥交换:如果证书有效,客户端和服务器开始使用SSL协议进行会话密钥交换。这个过程中,客户端和服务器协商一种加密算法,生成一个会话密钥,用于在传输过程中加密数据。

    数据传输:客户端和服务器使用会话密钥对数据进行加密和解密,以确保数据在传输过程中是安全的。

    在这个过程中,SSL证书的作用是验证服务器的身份,以确保数据传输的安全性。如果证书无效,客户端将无法验证服务器的身份,无法确定数据是否安全,因此将无法进行传输。通过使用SSL证书,可以有效保护数据传输的安全性,防止数据在传输过程中被窃取或篡改。

    客户端验证服务器证书的有效性是通过公开密钥基础结构(Public Key Infrastructure, PKI)实现的。PKI是一种安全体系结构,用于建立和管理数字证书的创建、分发和验证,以确保SSL证书的安全性。在客户端验证证书时,会进行以下几个步骤:

    验证证书的颁发机构:客户端会比对服务器返回的SSL证书中的颁发机构(CA)与其本地存储的根证书是否匹配,以确保颁发机构是可信的。

    验证证书的有效期:客户端会检查证书中的有效期,以确保证书未过期。

    检查证书中的域名:客户端会检查证书中的域名与其连接的服务器是否匹配,以确保证书属于正确的网站。

    检查证书的签名:客户端会使用证书颁发机构的公钥对证书进行解密,以确保证书上的数字签名是有效的。

    如果证书验证通过,客户端将开始建立加密连接并继续进行通信。如果验证失败,客户端将会发出警告,提示用户存在潜在的安全风险。

    总的来说,客户端会使用PKI验证证书的完整性、有效期和身份,确保SSL证书的可信度和安全性。如果证书无效,客户端将会发出警告,以保护用户的数据安全。

    4 JDK 中的SSLengine

    4.1  SSLengine运行流程

    JSSE封装了底层复杂的安全通信细节,使得开发人员能方便地用它来开发安全地网络应用程序。JSSE地API允许采用第三方提供地实现,该实现可作为插件集成到JSSE中。这些插件必须支持Oracle指定地加密套件。(加密套件包括一组加密参数,这些参数指定了加密算法地密钥长度等信息。)如:SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA采用SSL协议,密钥交换算法为DHE,加密算法为RSA

     JSSE的主要类图如下:

     
    image.png

    图4-1   JSSE的主要类图

    使用SSLengine进行通信的流程如下:

    **      client          server          message***
    
    **      ======          ======          =======***
    
    **      wrap()          ...             ClientHello***
    
    **      ...             unwrap()        ClientHello***
    
    **      ...             wrap()          ServerHello/Certificate***
    
    **      unwrap()        ...             ServerHello/Certificate***
    
    **      wrap()          ...             ClientKeyExchange***
    
    **      wrap()          ...             ChangeCipherSpec***
    
    **      wrap()          ...             Finished***
    
    **      ...             unwrap()        ClientKeyExchange***
    
    **      ...             unwrap()        ChangeCipherSpec***
    
    **      ...             unwrap()        Finished***
    
    **      ...             wrap()          ChangeCipherSpec***
    
    **      ...             wrap()          Finished***
    
    **      unwrap()        ...             ChangeCipherSpec***
    
    **      unwrap()        ...             Finished*
    

    4.2 运行demo分析SSLengine

    第一步 :借助keytool工具,进行密钥和证书管理,具体用法可参考JDK官方文档(docs.oracle.com/javase/8/do…)

    image.png

    1首先生成密钥对 同时指定存储该密钥的密钥库。

    image.png

    2导出公钥证书

    image.png

    3导入公钥证书到truststore仓库中
    image.png

    第二步:使用官网给的SSLengine的样例代码

    import javax.net.ssl.*;
    import javax.net.ssl.SSLEngineResult.*;
    import java.io.*;
    import java.security.*;
    import java.nio.*;
    
    public class SSLEngineSimpleDemo {
        private static boolean debug = true;
        private SSLContext sslc;
    
        private SSLEngine clientEngine; // client Engine  
        private ByteBuffer clientOut; // write side of clientEngine  
        private ByteBuffer clientIn; // read side of clientEngine  
    
        private SSLEngine serverEngine; // server Engine  
        private ByteBuffer serverOut; // write side of serverEngine  
        private ByteBuffer serverIn; // read side of serverEngine  
    
        /* 
         * For data transport, this example uses local ByteBuffers. This isn't 
         * really useful, but the purpose of this example is to show SSLEngine 
         * concepts, not how to do network transport. 
         */
        private ByteBuffer cTOs; // "reliable" transport client->server  
        private ByteBuffer sTOc; // "reliable" transport server->client  
    
        /* 
         * The following is to set up the keystores. 
         */
       private static String keyStoreFile = "这里填密钥仓库keystore的路径";
       private static String trustStoreFile = "这里填信任仓库truststore的路径";
    
        public static void main(String args[]) throws Exception {
            if (debug) {
                System.setProperty("javax.net.debug", "all");
            }
    
            SSLEngineSimpleDemo demo = new SSLEngineSimpleDemo();
            demo.runDemo();
    
            System.out.println("Demo Completed.");
        }
    
        public SSLEngineSimpleDemo() throws Exception {
    
            KeyStore ks = KeyStore.getInstance("JKS");
            KeyStore ts = KeyStore.getInstance("JKS");
    
            //password
            char[] passphrase = "password".toCharArray();
    
            File file = new File(keyStoreFile);
            System.out.println("put keystore in this path:"+file.getAbsolutePath());
    
            ks.load(new FileInputStream(keyStoreFile), passphrase);
            ts.load(new FileInputStream(trustStoreFile), passphrase);
    
            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
            kmf.init(ks, passphrase);
    
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
            tmf.init(ts);
            SSLContext sslCtx = SSLContext.getInstance("TLS");
            sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
            sslc = sslCtx;
        }
    
        private void runDemo() throws Exception {
            boolean dataDone = false;
    
            createSSLEngines();
            createBuffers();
    
            SSLEngineResult clientResult; 
            SSLEngineResult serverResult; 
            while (!isEngineClosed(clientEngine) || !isEngineClosed(serverEngine)) {
    
                log("================");
    
                clientResult = clientEngine.wrap(clientOut, cTOs);
                log("client wrap: ", clientResult);
                runDelegatedTasks(clientResult, clientEngine);
    
                serverResult = serverEngine.wrap(serverOut, sTOc);
                log("server wrap: ", serverResult);
                runDelegatedTasks(serverResult, serverEngine);
    
                cTOs.flip();
                sTOc.flip();
    
                log("----");
    
                clientResult = clientEngine.unwrap(sTOc, clientIn);
                log("client unwrap: ", clientResult);
                runDelegatedTasks(clientResult, clientEngine);
    
                serverResult = serverEngine.unwrap(cTOs, serverIn);
                log("server unwrap: ", serverResult);
                runDelegatedTasks(serverResult, serverEngine);
    
                cTOs.compact();
                sTOc.compact();  
    
                if (!dataDone && (clientOut.limit() == serverIn.position())
                        && (serverOut.limit() == clientIn.position())) {  
                    checkTransfer(serverOut, clientIn);
                    checkTransfer(clientOut, serverIn);
    
                    log("tClosing clientEngine's *OUTBOUND*...");
                    clientEngine.closeOutbound();
                    // serverEngine.closeOutbound();  
                    dataDone = true;
                }
            }
        }
    
        /* 
         * Using the SSLContext created during object creation, create/configure the 
         * SSLEngines we'll use for this demo. 
         */
        private void createSSLEngines() throws Exception {  
            /* 
             * Configure the serverEngine to act as a server in the SSL/TLS 
             * handshake. Also, require SSL client authentication. 
             */
            serverEngine = sslc.createSSLEngine();
            serverEngine.setUseClientMode(false);
            serverEngine.setNeedClientAuth(true);  
    
            /* 
             * Similar to above, but using client mode instead. 
             */
            clientEngine = sslc.createSSLEngine("client", 80);
            clientEngine.setUseClientMode(true);
        }
    
        /* 
         * Create and size the buffers appropriately. 
         */
        private void createBuffers() {  
    
            /* 
             * We'll assume the buffer sizes are the same between client and server. 
             */
            SSLSession session = clientEngine.getSession();
            int appBufferMax = session.getApplicationBufferSize();
            int netBufferMax = session.getPacketBufferSize();  
    
            /* 
             * We'll make the input buffers a bit bigger than the max needed size, 
             * so that unwrap()s following a successful data transfer won't generate 
             * BUFFER_OVERFLOWS. 
             *  
             * We'll use a mix of direct and indirect ByteBuffers for tutorial 
             * purposes only. In reality, only use direct ByteBuffers when they give 
             * a clear performance enhancement. 
             */
            clientIn = ByteBuffer.allocate(appBufferMax + 50);
            serverIn = ByteBuffer.allocate(appBufferMax + 50);
    
            cTOs = ByteBuffer.allocateDirect(netBufferMax);
            sTOc = ByteBuffer.allocateDirect(netBufferMax);
    
            clientOut = ByteBuffer.wrap("Hi Server, I'm Client".getBytes());
            serverOut = ByteBuffer.wrap("Hello Client, I'm Server".getBytes());
        }
    
        /* 
         * If the result indicates that we have outstanding tasks to do, go ahead 
         * and run them in this thread. 
         */
        private static void runDelegatedTasks(SSLEngineResult result,
                                              SSLEngine engine) throws Exception {
    
            if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
                Runnable runnable;
                while ((runnable = engine.getDelegatedTask()) != null) {
                    log("trunning delegated task...");
                    runnable.run();
                }
                HandshakeStatus hsStatus = engine.getHandshakeStatus();
                if (hsStatus == HandshakeStatus.NEED_TASK) {
                    throw new Exception("handshake shouldn't need additional tasks");
                }
                log("tnew HandshakeStatus: " + hsStatus);
            }
        }
    
        private static boolean isEngineClosed(SSLEngine engine) {
            return (engine.isOutboundDone() && engine.isInboundDone());
        }
    
        /* 
         * Simple check to make sure everything came across as expected. 
         */
        private static void checkTransfer(ByteBuffer a, ByteBuffer b)
                throws Exception {
            a.flip();
            b.flip();
    
            if (!a.equals(b)) {
                throw new Exception("Data didn't transfer cleanly");
            } else {
                log("tData transferred cleanly");
            }
    
            a.position(a.limit());
            b.position(b.limit());
            a.limit(a.capacity());
            b.limit(b.capacity());
        }
    
        /* 
         * Logging code 
         */
        private static boolean resultOnce = true;
    
        private static void log(String str, SSLEngineResult result) {
            if (!logging) {
                return;
            }
            if (resultOnce) {
                resultOnce = false;
                System.out.println("The format of the SSLEngineResult is: n"
                        + "t"getStatus() / getHandshakeStatus()" +n"
                        + "t"bytesConsumed() / bytesProduced()"n");
            }
            HandshakeStatus hsStatus = result.getHandshakeStatus();
            log(str + result.getStatus() + "/" + hsStatus + ", "
                    + result.bytesConsumed() + "/" + result.bytesProduced()
                    + " bytes");
            if (hsStatus == HandshakeStatus.FINISHED) {
                log("t...ready for application data");
            }
        }
    
        private static void log(String str) {
            if (logging) {
                System.out.println(str);
            }
        }
    }
    
    

    运行结果截图(部分):

    image.png

    第三步:代码分析

    SSLEngine封装了与安全通信有关的细节,它负责了底层ssl协议的握手、加密、解密、关闭会话等等操作,该类内部维护了安全通信的各个状态。由于安全通信过程中收发的数据都是加密后的数据,暂且将加密之后的密文数据称为网络数据,而加密之前的数据就称为应用程序数据。SSLEngine类提供了将应用程序数据加密为网络数据的API(wrap()打包)和将网络数据解密为网络数据的API(unwrap()解包),在执行打包时,该类会自己加入SSL握手数据,在解包时可以除去SSL握手数据。

    SSLEngine在握手过程中定义了5种HandshakeStatus状态(维护在SSLEngineResult类中,除FINISHED外其他四种都可以通过SSLEngine对象获取):

    NEED_UNWRAP:表示等待解包,即等待接收对端的数据。
    NEED_WRAP:表示等待打包,即将数据发送给对端。
    NEED_TASK:当解包或者打包完毕之后可能需要处理一些额外的任务,这些任务都是比较耗时或者可能阻塞的,例如访问密钥文件、连接远程证书认证服务、密钥管理器使用何种认证方式作为客户端认证等等操作。该状态是NEED_UNWRAP和NEED_WRAP之间中间状态。
    FINISHED:握手刚刚完成。SSLEngine握手一旦被触发,那么就不要不停的wrap和unwrap。这个时候是不需要应用程序数据的,它会自己交换一些握手需要的数据,比如认证、协商加密套件等等。通过数次wrap和unwrap之后直到状态变为FINISHED。(注意wrap()和unwrap()方法都会返回SSLEngineResult对象,FINISHED状态只能由该类获取)
    NOT_HANDSHAKING:当前不是握手状态。当握手完成时,会将状态设置为NOT_HANDSHAKING。

    注意:握手时是不需要应用程序数据的,就算存储应用程序数据的ByteBuffer是空的也没事。只不过wrap和unwrap方法定义如下:

    unwrap(ByteBuffer src, ByteBuffer dst) //dst存储应用程序数据

    wrap(ByteBuffer src, ByteBuffer dst)    //src存储应用程序数据

     不能将存储应用程序的ByteBuffer设置为null,但是它却可以是空的没数据。这个对握手没有影响。

    除HandshakeStatus之外,在SSLEngineResult类种还维护者另一个枚举类Status,它用于表示wrap和unwrap操作之后的结果状态:

    BUFFER_OVERFLOW:表示目标缓冲区容量不足无法存放解包之后的数据

    BUFFER_UNDERFLOW:表示没有足够的数据让SSLEngine来解包

    CLOSE:SSLEngine被关闭。(sslEngine.closeOutbound())

    OK:一切正常。

    在结合NIO编程时,当服务器接收到一个客户端的连接请求后,就要开始进行握手,这个过程是同步的,所以先不要吧read和write事件也注册到selector上,当完成握手后,才注册这两个事件,并把socket设置成非阻塞。当select到socket可读时先调用unwrap方法,可写时先调用wrap方法。

     

    image.png

     

    5 国密SSL证书

    5.1 国密算法和国密证书

    国密即国家密码局认定的国产密码算法。主要有SM1,SM2,SM3,SM4。密钥长度和分组长度均为128位。

    SM1 为对称加密。其加密强度与AES相当。该算法不公开,调用该算法时,需要通过加密芯片的接口进行调用。

    SM2为非对称加密,基于ECC(椭圆曲线密码算法)。该算法已公开。由于该算法基于ECC,故其签名速度与秘钥生成速度都快于RSA。ECC 256位(SM2采用的就是ECC 256位的一种)安全强度比RSA 2048位高,但运算速度快于RSA。

    SM3 消息摘要算法。可以用MD5作为对比理解,该算法已公开。校验结果为256位。

    SM4 无线局域网标准的分组数据算法。对称加密,密钥长度和分组长度均为128位。

    由于SM1、SM4加解密的分组大小为128bit,故对消息进行加解密时,若消息长度过长,需要进行分组,要消息长度不足,则要进行填充。

    国密SSL证书是遵循国家标准技术规范并参考国际标准,采用我国自主研发的SM2公钥算法体系,支持SM2、SM3、SM4等国产密码算法及国密SSL安全协议的数字证书。国密SSL证书采用自主可控的密码技术,能够满足政府机构、事业单位、大型国企、金融银行等重点领域用户对HTTPS协议国产化改造和国密算法应用的合规需求。

     

    5.2 国密SSL证书和国际SSL证书的区别

    1.加密算法不同

    国际算法SSL证书通常采用RSA或者ECC等加密算法,其中RSA是国际上应用最为普遍的加密算法,一般为2048位密钥长度,能够抵挡已知的绝大多数密码攻击。
    国密算法SSL证书通常采用国密算法(SM2/SM3/SM4),一般为256位密钥长度。相对于国际上常用的加密算法,国密算法结构和加密强度都有所不同。

    2.颁发机构不同

    国密SSL证书的颁发机构是由中国国家密码管理局(SAC)认证的证书颁发机构(如CFCA),而国际上主流的SSL证书则由多家全球性的知名证书颁发机构(如Symantec、Comodo、DigiCert、Sectigo等)颁发。

    3.兼容性不同

    国际算法SSL证书采用当下最有影响力和最常用的RSA公钥加密算法,支持所有主流浏览器和移动终端,能抵抗已知的绝大多数密码攻击。而由于国密算法还没有在所有主流浏览器中广泛兼容,一些仅支持国际算法的主流浏览器会对国密SSL证书报错。

     
    4.公网支持度不同

    使用国际算法SSL证书与国密算法SSL证书的网站,在公网上都可以打开。但当用户访问使用国密SSL证书的网站时,如果用户的浏览器不支持国密算法,就会提示用户进行升级或安装相应的组件。因此,在访问国密SSL证书的网站时,用户最好使用支持国密算法的浏览器。

     
    5.适用范围不同

    国密SSL证书的适用范围主要是在中国国内,尤其是在政府和金融领域中应用较多,而国际上主流的SSL证书则可以适用于全球范围内的网站和应用程序。这主要是因为国密SSL证书只能被国家密码管理局授权的厂商所颁发,因此使用国密SSL证书的网站在海外访问可能会遇到一些问题。有些国家可能会限制或限制使用国密算法的通信,因此,如果网站需要在海外访问,就更适合选择国际算法SSL证书。

     

    5.3 自签名证书

    自签名证书是由不受信的CA机构颁发的数字证书,也就是自己签发的证书
    (使用keytool或openssl等工具生成的证书都是自签名证书)。
    与受信任的CA签发的传统数字证书不同,自签名证书是由一些公司或软件开发商创建、颁发和签名的。虽然自签名证书使用的是与X.509证书相同的加密密钥对架构,但是却缺少受信任第三方的验证。在颁发过程中缺乏独立验证会产生额外的风险,这就是为什么对于面向公众的网站和应用程序来说,自签名证书是不安全的。

    自签名证书有什么优势?

    a 免费。自签名证书是免费提供的,任何开发人员都可以申请。

    b 随时签发。自签名证书可以随时随地签发,不用等待第三方证书颁发机构的验证和签发。

    c 加密。自签名SSL证书使用与其他付费SSL/TLS证书相同的方法加密传输数据。

    d 方便。自签名证书不会在一段时间后过期或需要续订,但CA颁发的证书却会在一段时间后过期,还需要续订。

    自签名证书有什么缺陷?

    a.不受浏览器信任,易丢失用户。

    每当用户访问使用自签名证书的站点时,他们会收到“不安全”警告,显示诸如“error_self_signed_cert”或“err_cert_authority_invalid”之类的错误,要求用户确认他们愿意承担风险继续浏览。这些警告会给网站访问者带来恐惧和不安,用户会认为该网站已被入侵,无法保护他们的数据,最后选择放弃浏览该站点转而访问不会提示安全警告的竞争对手网站。另外,不受浏览器信任的自签名证书,地址栏不会显示安全锁和HTTPS协议头。

    b.不安全。

    由于自签名证书支持超长有效期,因此也无法在发现新的漏洞后进行安全更新,容易受到中间人攻击破解。自签名SSL证书没有可访问的吊销列表,也容易被黑客伪造、假冒网站利用,不能满足当前的安全策略,存在诸多的不安全隐患。

    表5-1 自签名证书跟第三方证书的对比

    证书类型 优点 缺点 示例 适用场景
    第三方证书 安全、受用户信任 收取费用 GlobalSign签名证书DigiCert签名证书Entrust签名证书 各大网站服务器
    自签名证书 免费、方便 安全隐患、不受用户信任 openssl生成的证书、keytool生成的证书等 测试环境或限制外部人员访问的内部环境

     

    一般来说,自签名证书可以用于内部测试环境或限制外部人员访问的Web服务器。如果企业有内网SSL证书、用户证书、S/MIME证书、设备证书的数字认证需求,可以考虑自建CA服务,它可让您轻松安全地管理自签发证书且无需高额的前期设备和系统投资,能够有效的降低维护成本,满足企业内部数字认证需求。

     

    参考内容:

    [1] zhuanlan.zhihu.com/p/34753269

    [2] blog.wangriyu.wang/2018/03-htt…

    [3] datatracker.ietf.org/doc/html/rf…

    [4] www.gmssl.cn/gmssl/index… (国密SSL)

    [5] www.racent.com/blog/what-i…

    (自签名证书的优缺点)

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论