5.4. handshake — Server Certificate
当服务器确定了CipherSuite后,根据CipherSuite里面的认证算法,如果需要发送证书给客户端,那么就发送 Server Certificate消息给客户端。Server Certificate总是在ServerHello之后立即发送,所以在同一个RTT里。
Server Certificate里面包含了服务器的证书链。
消息结构:
opaque ASN.1Cert<1..2^24-1>;struct {
ASN.1Cert certificate_list<0..2^24-1>;
} Certificate;
certificate_list: 证书列表,发送者的证书必须是第一个,后续的每一个证书都必须是前一个的签署证书。根证书可以省略
证书申请的时候,一般会收到好几个证书,有的需要自己按照这个格式来拼接成证书链。
如果服务器要认证客户端的身份,那么服务器会发送Certificate Request消息,客户端应该也以 这条Server Certificate消息的格式回复。
服务器发送的证书必须:
证书类型必须是 X.509v3。除非明确地协商成别的了(比较少见,rfc里提到了例如 [OpenPGP格式] 链接 https://tools.ietf.org/html/rfc5081 )。
服务器证书的公钥,必须和选择的密钥交换算法配套。
密钥交换+认证算法配套的证书中公钥类型
RSA / RSA_PSKRSA 公钥;证书中必须允许私钥用于加密 (即如果使用了X509V3规定的key usage扩展, keyEncipherment比特位必须置位) 这种用法没有前向安全性,因此在 TLS 1.3中被废弃了
DHE_RSA / ECDHE_RSARSA 公钥;证书中必须允许私钥用于签名(即如果使用了X509V3规定的key usage扩展, digitalSignature比特位必须置位),并且允许server key exchange消息将要使用的签名模式(例如 PKCS1_V1.5 ,OAEP等)和hash算法(例如sha1, sha256等)
DHE_DSSDSA 公钥; 历史遗留产物,从来没有被大规模用过,安全性差,废弃状态。证书必须允许私钥用于签名,必须允许server key exchange消息中使用的hash算法。
DH_DSS / DH_RSADiffie-Hellman 公钥; 要求key usage里面的keyAgreement比特位必须置位。 这种用法没有前向安全性,因此在 TLS 1.3中被废弃了
ECDH_ECDSA / ECDH_RSA能做 ECDH 用途的公钥;公钥必须使用 客户端支持的ec曲线和点格式。这种用法没有前向安全性,因此在 TLS 1.3中被废弃了
ECDHE_ECDSAECDSA用途的公钥;证书必须运输私钥用作签名,必须允许server key exchange消息里面要用到的hash算法。公钥必须使用客户端支持的ec曲线和点格式。
“server_name” 和 “trusted_ca_keys” 扩展用于引导证书选择。
其中有5种是ECC密钥交换算法:ECDH_ECDSA, ECDHE_ECDSA, ECDH_RSA, ECDHE_RSA, ECDH_anon。ECC(椭圆曲线)体制相比RSA,由于公钥更小,性能更高,所以在移动互联网环境下越发重要。以上ECC的5种算法都用ECDH来计算premaster secret, 仅仅是ECDH密钥的生命周期和认证算法不同。其中只有 ECDHE_ECDSA 和 ECDHE_RSA 是前向安全的。
如果客户端在ClientHello里提供了 “signature_algorithms” 扩展,那么服务器提供的所有证书必须用 “signature_algoritms”中提供的 hash/signature算法对 之一签署。要注意的是,这意味着,一个包含某种签名算法密钥的证书,可能被另一种签名算法签署(例如,一个RSA公钥可能被一个ECDSA公钥签署)。(这在TLS1.2和TLS1.1中是不一样的,TLS1.1要求所有的算法都相同。)注意这也意味着DH_DSS,DH_RSA,ECDH_ECDSA,和ECDH_RSA 密钥交换不限制签署证书的算法。固定DH证书可能使用”signature_algorithms”扩展列表中的 hash/签名算法对 中的某一个签署。名字 DH_DSS, DH_RSA, ECDH_ECDSA, 和 ECDH_RSA 只是历史原因,这几个名字的后半部分中指定的算法,并不会被使用,即DH_DSS中的DSS并不会被使用,DH_RSA中并不会使用RSA做签名,ECDH_ECDSA并不会使用ECDSA算法。。。如果服务器有多个证书,就必须从中选择一个,一般根据服务器的外网ip地址,SNI中指定的hostname,服务器配置来做选择。如果服务器只有一个证书,那么要确保这一个证书符合这些条件。要注意的是,存在一些证书使用了TLS目前不支持的 算法组合。例如,使用 RSASSA-PSS签名公钥的证书(即证书的SubjectPublicKeyInfo字段是id-RSASSA-PSS)。由于TLS没有给这些算法定义对应的签名算法,这些证书不能在TLS中使用。如果一个CipherSuite指定了新的TLS密钥交换算法,也会指定证书格式和要求的密钥编码方法。
5.5. handshake — Server Key Exchange
服务器会在 server Certificate 消息之后,立即发送 Server Key Exchange消息。(如果协商出的CipherSuite不需要做认证,即anonymous negotiation,会在ServerHello之后立即发送Server Key Exchange消息)
只有在server Certificate 消息没有足够的信息,不能让客户端完成premaster的密钥交换时,服务器才发送 server Key Exchange, 主要是对前向安全的几种密钥协商算法,列表如下:
DHE_DSS
DHE_RSA
DH_anon
ECDHE_ECDSA
ECDHE_RSA
ECDH_anon
对下面几种密钥交换方法,发送ServerKeyExchange消息是非法的:
RSA
DH_DSS
DH_RSA
ECDH_ECDSA
ECDH_RSA
需要注意的是,ECDH和ECDSA公钥的数据结构是一样的。所以,CA在签署一个证书的时候,可能要使用 X.509 v3 的 keyUsage 和 extendedKeyUsage 扩展来限定ECC公钥的使用方式。
ServerKeyExchange传递足够的信息给客户端,来让客户端交换premaster secret。一般要传递的是:一个 Diffie-Hellman 公钥,或者一个其他算法(例如RSA)的公钥。
在TLS实际部署中,我们一般只使用这4种:ECDHE_RSA, DHE_RSA, ECDHE_ECDSA,RSA
其中RSA密钥协商(也可以叫密钥传输)算法,由于没有前向安全性,在TLS 1.3里面已经被废除了。参见: [Confirming Consensus on removing RSA key Transport from TLS 1.3 ] 链接 http://www.ietf.org/mail-archive/web/tls/current/msg12266.html
消息格式:
enum { dhe_dss, dhe_rsa, dh_anon, rsa, dh_dss, dh_rsa, ec_diffie_hellman
} KeyExchangeAlgorithm;struct {
opaque dh_p<1..2^16-1>;
opaque dh_g<1..2^16-1>;
opaque dh_Ys<1..2^16-1>;
} ServerDHParams; /* Ephemeral DH parameters */dh_p
Diffie-Hellman密钥协商计算的大质数模数。
dh_g
Diffie-Hellman 的生成元,
dh_Ys
服务器的Diffie-Hellman公钥 (g^X mod p). struct {
opaque point <1..2^8-1>;
} ECPoint; enum { explicit_prime (1), explicit_char2 (2),
named_curve (3), reserved(248..255) } ECCurveType; struct {
ECCurveType curve_type;
select (curve_type) { case named_curve:
NamedCurve namedcurve;
};
} ECParameters; struct {
ECParameters curve_params;
ECPoint public; //ECDH的公钥
} ServerECDHParams;struct {
select (KeyExchangeAlgorithm) { case dh_anon:
ServerDHParams params; case dhe_dss: case dhe_rsa:
ServerDHParams params;
digitally-signed struct {
opaque client_random[32];
opaque server_random[32];
ServerDHParams params;
} signed_params; case ec_diffie_hellman:
ServerECDHParams params;
Signature signed_params; case rsa: case dh_dss: case dh_rsa: struct {} ; /* message is omitted for rsa, dh_dss, and dh_rsa */
/* may be extended, e.g., for ECDH -- see [TLSECC] */
};
} ServerKeyExchange;
params
服务器的密钥交换参数。
signed_params
对需要认证的(即非anonymous的)密钥交换,对服务器的密钥交换参数的数字签名。
ECParameters 结构比较麻烦,其中ECCurveType是支持3种曲线类型的,可以自行指定椭圆曲线的多项式系数,基点等参数。但是,我们基本不会用到这种功能,因为一般部署都是使用 NamedCurve,即参数已经预先选定,各种密码学库普遍都支持的一组曲线,其中目前用的最广的是 secp256r1 (还被称为 P256,或 prime256v1)
NamedCurve 列表中比较重要的曲线(在TLS1.3中,只保留了这几条曲线。),定义如下:
enum {
...
secp256r1 (23), secp384r1 (24), secp521r1 (25),
reserved (0xFE00..0xFEFF),
(0xFFFF)
} NamedCurve;
ECDHE_RSA 密钥交换算法的 SignatureAlgorithm 是 rsa 。ECDHE_RSA 密钥交换算法的 SignatureAlgorithm 是 ecdsa。
如果客户端提供了 “signature_algorithms” 扩展, 则签名算法和hash算法必须是列在扩展中的算法。要注意的是,这个地方可能有不一致,例如客户端可能提供了 DHE_DSS 密钥交换,但是 “signature_algorithms”扩展中没有DSA算法,在这类情况下,为了正确地协商,服务器必须确保满足自己选择的CipherSuite满足 “signature_algorithms” 的限制。这不优雅,但是是为了把对原来的CipherSuite协商的设计的改动减到最小,而做的妥协。
并且,hash和签名算法,必须和服务器的证书里面的公钥兼容。
5.6. handshake — Certificate Request
TLS规定了一个可选功能:服务器可以认证客户端的身份,这通过服务器要求客户端发送一个证书实现,服务器应该在ServerKeyExchange之后立即发送CertificateRequest消息。
消息结构:
enum {
rsa_sign(1), dss_sign(2), rsa_fixed_dh(3),dss_fixed_dh(4),
rsa_ephemeral_dh_RESERVED(5),dss_ephemeral_dh_RESERVED(6),
fortezza_dms_RESERVED(20),
ecdsa_sign(64), rsa_fixed_ecdh(65),
ecdsa_fixed_ecdh(66),
(255)
} ClientCertificateType;
opaque DistinguishedName<1..2^16-1>;struct {
ClientCertificateType certificate_types<1..2^8-1>;
SignatureAndHashAlgorithm
supported_signature_algorithms<2^16-1>;
DistinguishedName certificate_authorities<0..2^16-1>;
} CertificateRequest;
certificate_types: 客户端可以提供的证书类型。
rsa_sign 包含RSA公钥的证书。
dss_sign 包含DSA公钥的证书。
rsa_fixed_dh 包含静态DH公钥的证书。
dss_fixed_dh 包含静态DH公钥的证书。
supported_signature_algorithms: 服务器支持的 hash/signature 算法的列表。
certificate_authorities: 服务器可以接受的CA(certificate_authorities)的 distinguished names 的列表 DER编码格式.
这些 distinguished names 可能为root CA或者次级CA指定了想要的 distinguished name ,因此,这个消息可以用来描述已知的root,或者希望的授权空间。如果 certificate_authorities 列表是空的,那么客户端可以发送任何适当的 ClientCertificateType 类型的证书,如果没有别的限制的话。
certificate_types 和 supported_signature_algorithms 字段的交叉选择很复杂。 certificate_types 这个字段从SSLv3时代就定义了,但是一直都没有详细定义,其大多数功能都被 supported_signature_algorithms 代替了。 有如下规则:
客户端提供的任何证书,必须用一个supported_signature_algorithms 中出现过的 hash/signature 算法对 签名.
客户端提供的末端证书必须提供一个和 certificate_types 兼容的key。 如果这个key是一个签名key,那必须能和 supported_signature_algorithms 中提供的某个 hash/signature 算法对配合使用。
由于历史原因,某些客户端证书类型的名字,包含了证书的签名算法,例如,早期版本的TLS中, rsa_fixed_dh 意思是一个被RSA算法签署,并且包含一个固定DH密钥的证书。在TLS1.2中,这个功能被 supported_signature_algorithms 淘汰,并且证书类型不再限制用来签署证书的算法。例如,如果服务器发送了 dss_fixed_dh 证书类型,和 { {sha1, dsa}, {sha1,rsa} } 签名类型,客户端可以回复一个 包含静态DH密钥,用RSA-sha1签署的证书。
如果协商出来的是匿名CipherSuite,服务器不能要求客户端认证。
5.7. handshake — Server Hello Done
在 ServerHello和相关消息已经处理结束后,服务器发送ServerHelloDone。在发送ServerHelloDone后,服务器开始等待客户端的响应。
ServerHelloDone消息表示,服务器已经发送完了密钥协商需要的消息,并且客户端可以开始客户端的密钥协商处理了。
收到ServerHelloDone后,客户端应该确认服务器提供了合法的证书,并且确认服务器的ServerHello消息里面的参数是可以接受的。
消息格式:
struct { } ServerHelloDone;
5.8. handshake — Client Certificate
ClientCertificate消息是客户端收到ServerHelloDone后,可以发送的第一条消息。仅当服务器要求了一个证书的情况下,客户端才发送ClientCertificate消息,如果没有可用的合适证书,客户端必须发送一条不包含任何证书的ClientCertificate消息(即 certificate_list 结构长度为0)。
如果客户端没有发送任何证书,服务器自行决定,可以放弃要求客户端认证,继续握手;或者发送一条 fatal handshake_failure的alert消息,断开连接。并且,如果证书链的某些方面是不能接受的(比如证书没有被可信任的CA签署),服务器可以自行决定,是继续握手(放弃要求客户端认证),或者发送一条fatal的alert。
客户端证书使用和ServerCertificate相同的结构发送。
ClientCertificate把客户端的证书链发送给服务器。服务器会使用证书链来验证CertificateVerify 消息(如果使用基于签名的客户端认证),或者来计算premaster secret(对于非短暂的 DH)。证书必须和协商出来的CipherSuite的密钥交换算法配套,并和任何协商的扩展配套。
尤其是:
证书必须是X.509v3 类型的。
客户端的末级证书的公钥必须和CertificateRequest里列出的证书类型兼容。
客户端证书类型证书公钥类型
rsa_signRSA公钥;证书必须允许公钥用于certificateVerify消息中的数字签名和hash算法
dss_signDSA 公钥;证书必须允许密钥使用CertificateVerify中的hahs函数做签名;
ecdsa_sign可以用作 ECDSA 的公钥;证书必须允许 公钥用 CertificateVerify中的hash函数做签名;公钥必须使用服务器支持的曲线,和点格式;
rsa_fixed_dh / dss_fixed_dhDiffie-Hellman 公钥; 必须使用和服务器key相同的参数。
rsa_fixed_ecdh / ecdsa_fixed_ecdh可以用作 ECDH 的公钥。必须和服务器的公钥使用同样的曲线,同样的点格式
如果 certificate_authorities 列表不是空的,客户端证书链中的某一个证书必须是CA中的某一个签署的。
证书必须使用 服务器可以接受的 hash/signature 算法对。
类似于Server Certificate,有一些证书目前无法在TLS中使用。
5.9. handshake — Client Key Exchange
客户端必须在客户端的Certificate消息之后,立即发送ClientKeyExchange消息。或者必须在ServerHelloDone后立即发送ClientKeyExchange消息。
ClientKeyExchange消息中,会设置premaster secret,通过发送 RSA公钥加密premaster secret的密文,或者发送允许双方得出相同的premaster secret的Diffie-Hellman参数。
当客户端使用短暂的 Diffie-Hellman 密钥对时,ClientKeyExchange包含客户端的 Diffie-Hellman 公钥。如果客户端发送一个包含静态 Diffie-Hellman 指数的证书(比如,在使用固定DH的客户端认证),那么这条消息必须被发送,并且必须为空。
消息结构:消息的选择取决于选择的密钥交换算法。
struct {
select (KeyExchangeAlgorithm) { case rsa:
EncryptedPreMasterSecret; case dhe_dss: case dhe_rsa: case dh_dss: case dh_rsa: case dh_anon:
ClientDiffieHellmanPublic; case ec_diffie_hellman:
ClientECDiffieHellmanPublic;
} exchange_keys;
} ClientKeyExchange;
5.9.(1). RSA 加密的 Premaster Secret 消息
如果用RSA做密钥协商和认证,客户端生成 48字节的 premaster secret,使用服务器证书里面的公钥加密,然后把密文EncryptedPreMasterSecret发送给服务器,结构定义如下:
struct {
ProtocolVersion client_version;
opaque random[46];
} PreMasterSecret;
client_version
客户端支持的最新协议版本号,这个字段用来检测中间人版本回退攻击。T
random 46 字节的,安全生成的随机值。struct { public-key-encrypted PreMasterSecret pre_master_secret;
} EncryptedPreMasterSecret;
pre_master_secret
这个随机值由客户端生成,用于生成master secret。
注:PreMasterSecret里面的 client_version 是 ClientHello.client_version,而不是协商的到的版本号,这个特性用来阻止版本回退攻击。不幸的是,有些不正确的老的代码使用了协商得到的版本号,导致检查client_version字段的时候,和正确的实现无法互通。
客户端实现必须在PreMasterSecret中发送正确的版本号。如果 ClientHello.client_version 的版本号是 TLS 1.1 或者更高,服务器实现必须如下检查版本号。如果版本号是 TLS 1.0 或者更早,服务器必须检查版本号,但是可以通过配置项关闭检查。
要注意的是,如果版本号检查失败了,PreMasterSecret 应该像下面描述的那样填充成随机数。
TLS中的RSA使用的是 PKCS1-V1.5 填充( PKCS1-V1.5也是openssl库RSA的默认填充方式)。Bleichenbacher 在1998年发表了一种针对 PKCS1-V1.5 的选择密文攻击, Klima在2003年发现 PKCS1-V1.5 中 PreMasterSecret 版本号检查的一个侧通道攻击。只要TLS 服务器暴露一条特定的消息是否符合PKCS1-V1.5格式,或暴露PreMasterSecret解密后结构是否合法,或版本号是否合法,就可以用上面2种方法攻击。
Klima 还提出了完全避免这类攻击的方法:对格式不正确的消息,版本号不符的情况,要做出和完全正确的RSA块一样的响应,要让客户端区分不出这3种情况。具体地说,要如下:
生成 46 字节的密码学安全随机值 R
解密消息,获得明文 M
如果 PKCS#1 填充不正确,或者 PreMasterSecret 消息的长度不是48字节,则pre_master_secret = ClientHello.client_version || R或者如果 ClientHello.client_version <= TLS 1.0,并且明确禁止了版本号检查,则pre_master_secret = ClientHello.client_version || M[2..47]
注意:明确地用 ClientHello.client_version 构造 pre_master_secret 时,当客户端在原来的 pre_master_secret 中发送了错误的 客户端版本值时,会产生一个不合法的 master_secret 。
另一种解决问题的方法是,把版本号不符,当成 PKCS-1 格式错误来对待,并且完全随机填充 premaster secret。
生成 48 字节的密码学安全随机值 R
解密 PreMasterSecret 恢复出明文 M
如果 PKCS#1 填充不正确,或者消息的长度不是48字节,则pre_master_secret = R或者如果 ClientHello.client_version <= TLS 1.0,并且 明确禁止了版本号检查,则pre_master_secret = M或者如果 M[0..1] != CleintHello.client_versionpre_master_secret = R或者pre_master_secret = M
尽管实践中,还没有发现针对这种结构的攻击,Klima 在论文中描述了几种理论上的攻击方式,因此推荐上述的第一种结构。
在任何情况下,一个 TLS 服务器绝对不能在:1. 处理 RSA 加密的 premaster 消息失败, 2.或者版本号检查失败 时产生alert消息。当遇到这两种情况时,服务器必须用随机生成的 premaster 值继续握手。服务器可以把造成失败的真实原因log下来,用于调查问题,但是必须小心确保不能把这种信息泄漏给攻击者(比如通过时间侧通道,log文件,或者其它通道等泄漏)。
RSAES-OAEP 加密体制,更能抵抗 Bleichenbacher 发表的攻击,然而,为了和早期的TLS版本最大程度保持兼容,TLS 仍然规定使用 RSAES-PKCS1-v1_5 体制。只要遵守了上面列出的建议,目前还没有 Bleichenbacher 的变化形式能攻破 TLS 。
实现的时候要注意:公钥加密的数据用 字节数组 <0..2^16-1> 的形式表示。因此,ClientKeyExchange中的 RSA加密的PreMasterSecret 前面有2个字节用来表示长度。这2个字节在使用RSA做密钥协商时,是冗余的,因为此时 EncryptedPreMasterSecret 是 ClientKeyExchange 中的唯一字段,因此可以无歧义地得出 EncryptedPreMasterSecret 的长度。因此更早的 SSLv3 规范没有明确规定 public-key-encrypted 数据的编码格式,因此有一些SSLv3的实现没有包含 长度字段,这些实现直接把 RSA 加密的数据放入了 ClientKeyExchange消息里面。TLS规范要求 EncryptedPreMasterSecret 字段包含长度字段。因此得出的结果会和一些 SSLv3 的实现不兼容。实现者从 SSLv3 升级到 TLS 时,必须修改自己的实现,以接受并且生成带长度的格式。如果一个实现要同时兼容 SSLv3 和 TLS,那就应该根据协议版本确定自己的行为。
注意:根据 Boneh 等在2003年USENIX Security Symposium上发表的论文 “Remote timing attacks are practical”,针对 TLS RSA密钥交换的远程时间侧通道攻击,是实际可行的,起码当客户端和服务器在同一个LAN里时是可行的。因此,使用静态 RSA 密钥的实现,必须使用 RSA blinding,或者Boneh论文中提到的,其他抵抗时间侧通道攻击的技术。
openssl中的RSA blinding,参见:http://linux.die.net/man/3/rsa_blinding_on
5.9.(2). 客户端 Diffie-Hellman 公钥
这条消息把客户端的 Diffie-Hellman 公钥 ( Yc ) 发送给服务器。
Yc的编码方式由 PublicValueEncoding 决定。
消息的结构:
enum { implicit, explicit } PublicValueEncoding;
implicit
如果客户端已经发送了一个包含合适的 DH 公钥的证书(即 fixed_dh 客户端认证方式),那么Yc已经隐式包含了,不需要再发送。这种情况下,ClientKeyExchange消息必须发送,并且必须是空的。explicit
表示Yc需要发送。struct {
select (PublicValueEncoding) { case implicit: struct { }; case explicit: opaque dh_Yc<1..2^16-1>;
} dh_public;
} ClientDiffieHellmanPublic;
dh_Yc
客户端的 Diffie-Hellman 公钥 Yc.
5.9.(3). 客户端 EC Diffie-Hellman 公钥
struct {
select (PublicValueEncoding) { case implicit: struct { }; case explicit: ECPoint ecdh_Yc;
} ecdh_public;
} ClientECDiffieHellmanPublic;
Diffie-Hellman 推广到椭圆曲线群上,就是 EC Diffie-Hellman ,简称 ECDH,其它的计算,和一般的 DH 计算类似。
ECDH 是目前最重要的密钥协商算法 http://vincent.bernat.im/en/blog/2011-ssl-perfect-forward-secrecy.html
5.10. handshake — Cerificate Verify
当需要做客户端认证时,客户端发送CertificateVerify消息,来证明自己确实拥有客户端证书的私钥。这条消息仅仅在客户端证书有签名能力的情况下发送(就是说,除了含有固定 Diffie-Hellman 参数的证书以外的证书)。CertificateVerify必须紧跟在ClientKeyExchange之后发送。
消息结构: Structure of this message:
struct {
digitally-signed struct {
opaque handshake_messages[handshake_messages_length];
}
} CertificateVerify;
此处, handshake_messages 表示所有发送或者接收的握手消息,从client hello开始,一直到CertificateVerify之前的所有消息,包括handshake消息的type和length字段,这是之前所有握手结构体的拼接。要注意,这要求双方在握手过程中,都得缓存所有消息,或者在握手过程中,用每一种可能的hash算法计算到CeritificateVerify为止的hash值。
signature中用的hash和签名算法必须是 CertificateRequest 的 supported_signature_algorithms 中的某一种。另外,hash和签名算法必须和客户端的证书的算法兼容。RSA公钥可能被用于任何允许的hash函数,只要遵循证书中的限制。
本文转自微信后台团队,如有侵犯,请联系我们立即删除
OpenIMgithub开源地址:
https://github.com/OpenIMSDK/Open-IM-Server
OpenIM官网 : https://www.rentsoft.cn
OpenIM官方论坛: https://forum.rentsoft.cn/
更多技术文章:
开源OpenIM:高性能、可伸缩、易扩展的即时通讯架构https://forum.rentsoft.cn/thread/3
【OpenIM原创】简单轻松入门 一文讲解WebRTC实现1对1音视频通信原理https://forum.rentsoft.cn/thread/4
【OpenIM原创】开源OpenIM:轻量、高效、实时、可靠、低成本的消息模型https://forum.rentsoft.cn/thread/1