1. 为什么
ticket给TLS提供一个不需要在server存储会话状态(session)的机制来恢复会话。适用于TLS1.0, 1.1, 1.2。在以下情况下很有用:
- server需要处理大量不同用户的session;
- server希望长时间存储session;
- 需要使用跨server的负载均衡;
- 在内存很少的嵌入式的server上。
之前使用session ID的方法需要server将各种加密参数保存在本地,client下次希望恢复session的时候,就将session ID一并带上,server根据session ID查找本地缓存,找到对应的参数,然后直接建立握手。对照上面几个情况,看session ID为什么不适用:
- server处理大量会话需要在本地保存很多加密参数,内存占用和查找效率都会受影响;
- 长时间存储session导致长时间占用内存;
- session通常保存在一个server上,如果跨server负载均衡,需要将session统一存储,或支持跨server查询;
- 内存占用。
2. 是什么
2.1. 主要流程
- 第一次完整握手,client在ClientHello中发送一个空的session_ticket扩展,表示client支持ticket会话恢复机制,
- server回复一个空的session_ticket扩展,表示自己将会发送一个新的ticket。server会在发送CCS之前发送一个NewSessionTicket的消息,里边存放着用于会话恢复的各种参数,并加密。
- client收到后将ticket和本地的session一并存储。
- 第二次会话恢复,client在ClientHello中发送不为空的session_ticket扩展,表示client希望进行会话恢复;
- server收到后,解密校验,如果校验正确,就使用该session参数进行会话恢复。
- 不管server接受不接受client发送的ticket,只要server觉得需要签发一个新的ticket,都要发送session_ticket扩展,也就是说,ServerHello中的session_ticket就是指示server是否发送NewSessionTicket的。
server签发ticket的过程如下:
2.2. ticket的生命周期
开始:
- client从server接收新签发的ticket,保存到本地。
结束:
- ticket在client的缓存中超时;
- ticket由client的本地策略提前结束使用;
- server新签发了ticket,需要更新本地的ticket;
- 会话恢复时出错的时,需要删除ticket。
2.3. ticket的相关结构
1 | struct { |
其中:
ticket_lifetime_hint
: server签发的该ticket的存活时间,从client收到NewSessionTicket开始计算,client以此判断该ticket的超时时间,单位是秒。ticket
: server签发的ticket的具体内容,包含加密的一些参数。key_name
: 表示该ticket用的是哪组秘钥加密认证的,通过该参数,server能很容易判断该ticket是不是自己签发的。通常是在server启动时随机生成的。iv
: server使用AES128-CBC模式去加密encrypted_state
,用到的IV保存在这里,IV每次签发ticket的时候都随机生成,每个ticket都不一样。-encrypted_state
:使用AES128-CBC和iv
加密的实际session参数。mac
: server使用HMAC-SHA256生成消息认证码MAC,认证的消息为:key_name + iv + encrypted_state_len + encrypted_state
。- server加密需要两个秘钥——AES128-CBC使用的加密秘钥,HMAC-SHA256使用的认证秘钥。
其中encrypted_state
中没有规定,建议是:
1 | struct { |
其中:
protocol_version
: 签发该ticket时候使用的协议版本。cipher_suite
: 签发该ticket时候使用的加密套件。master_secret
: 签发该ticket时候使用的主秘钥。timestamp
: server用来判断ticket是否超时的。client_identity
: 如果进行了client认证,这里会包含一些对client认证的消息。certificate_based
: client认证使用了证书,这里就包含client的证书列表psk_identity
: client使用了PSK认证,这里就包含了psk的id,用该id可以查找到psk的相关信息。
2.4. ticket跟原有的session ID机制的交互使用
- 第一次握手,如果server表示会签发新的ticket,session ID就为空;
- 第二次握手,server拒绝使用ticket,可以发送非空的session ID表示自己支持有状态的会话恢复;
- client收到NewSessionTicket的话,就丢掉任何从ServerHello中获取的session ID;
- client发送ticket的时候,也可以生成一个session ID放在CH中; server如果接受ticket,也可以在SH中返回同样的session ID,这样,client就能区分server什么时候在进行会话恢复,什么时候回退到完整握手——server接受了ticket,就使用ticket进行会话恢复,此时session ID跟client发送的一样,server不接受ticket,如果返回跟client一样的session ID,就表示自己支持有状态的会话恢复,如果返回空,就表示自己不支持会话恢复,会回退到完整握手。同时,因为这个session ID是client自己生成的,所以server不接受ticket的话,也查不到session,只能回复空的session ID,client就能根据session ID是否为空,来判断是否进行会话恢复。
- client发送ticket的时候,server禁止使用session ID进行有状态的恢复;
- 总结起来就是:有限使用ticket机制。如果client自己生成了session ID,同时又发送了ticket,server也表示要用该ticket恢复,就返回同样的session ID。
3. 安全性
- 恢复失败的session会同时将ticket失效。
- 被盗的ticket也无所谓,因为ticket是加密的
- 伪造的ticket会导致握手失败
- DoS攻击的话,使用
key_name
可以抵御一部分,另外对ticket的解密校验尽量轻量化,比如使用对称加密算法。 - 加密ticket的key的管理建议:
- key需要随机生成;
- key最少128位;
- key除了加密和校验ticket,不能用作他用;
- key应该周期性更新;
- ticket格式或加密算法更新的话,key需要同时更新。
- ticket的有效时间可能超过24小时;
4. TLS1.3的ticket机制
TLS1.3将有状态和无状态的会话恢复机制都整合到PSK机制中,当然,PSK也跟最开始诞生的功能有些不一样了,所以TLS1.3的PSK集中了原有的PSK功能、有状态的会话恢复功能(session ID)、无状态的会话恢复功能(ticket)。现在只简单说下PSK的会话恢复功能,TLS1.3的PSK的完整介绍后续再说。
TLS1.3的会话恢复完全抛弃了CH和SH中session_id字段的功能,但为了兼容考虑,还会在会话恢复的时候进行相应的设置——TLS1.3的client跟TLS1.2的server协商的时候。
4.1. 主要流程
- 第一次握手,server如果支持会话恢复,就在握手完成后,发送
NewSessionTicket
消息; - client如果收到
NewSessionTicket
消息,就将当前session和其中的ticket存储到一起; - 第二次握手,client在CH中发送
pre_shared_key
扩展项,其中携带ticket。 - server收到psk扩展后,先解密校验ticket,然后用ticket中的PSK相关key去校验psk。
- server在握手完成后,还可能再签发一个新的
NewSessionTicket
用于替换之前的。
4.2. 消息格式
4.2.1. server新签发的ticket的NewSessionTicket
的消息格式为:
1 | struct { |
其中:
ticket_lifetime
: ticket从签发开始的有效时间,单位是秒。server不能签发超过7天(604800s)的ticket。为0表示该ticket应该立即丢掉。由于往返时间,server可以认为稍微超过一段有效时间的过期ticket仍然有效;ticket_age_add
: 每次签发ticket都生成的一个随机数。用于隐藏CH中pre_shared_key
扩展项中的ticket的有效时间,防止攻击者关联起多个连接。psk中的有效时间 = (client自己保存的ticket有效时间(单位毫秒) + ticket_age_add ) module 2^32。ticket_nonce
: 该连接上签发的ticket的唯一标识,从0开始依次递增;ticket
:用做CH中psk的identity,可以是一个索引(类似session ID一样的,这时候就是有状态的会话恢复)或者加密签名后的数据(类似原始的ticket,这时候就是无状态的会话恢复)。无状态的数据结构类似TLS1.2的ticket。extensions
:TLS1.3现在只定义了一个扩展项——early_data
,用于表示server最大能接受的早期数据大小(未加密的明文数据,不包括填充等,纯用户数据长度)。
4.2.2. PSK扩展项
1 | struct { |
其中:
identity
: 就是NewSessionTicket.ticket
。obfuscated_ticket_age
:隐藏的ticket有效时间,外部导入的PSK该值是0,上次会话建立的PSK的有效时间 = (client自己保存的ticket有效时间(单位毫秒) + ticket_age_add ) module 2^32。注意该值是以ms为单位。identities
: cilent希望用的一个identity列表,要是有0-RTT数据,必须用第一个identity(序号是0);binders
: 一列HMAC的值,按照identities
的顺序依次排列。目的是将psk(ticket或者sesssion ID或者外部导入的PSK)跟当前握手绑定。selected_identity
: server如果使用psk会话恢复,返回选择的identity的序号(从0开始)。
4.3. TLS1.3中ticket的安全性考虑
具体跟0-RTT的早期数据相关,后续再补。
5. openssl中关于ticket的相关接口
5.1. 处理ticket中的加密数据
1 | #include <openssl/tls1.h> |
因为没有必要为每一个session都维护一个单独的加密状态,所以就交给用户去维护,然后用户负责ticket中部分参数的生成和和状态维护。
server收到client发来的空的session_ticket
扩展项,enc
参数为1,表示这是要签发一个新的ticket,应用需要设置key_name, iv, ctx, hctx
给lib,lib会使用这些信息去创建并加密ticket。
server收到client发来的非空的session_ticket
扩展项,enc
参数为0, 表示这是要解析一个ticket,lib会传给应用key_name, iv
,应用需要设置ctx, hctx
给lib,用以解密校验ticket。
返回值表示应用是否希望签发使用新的ticket:
- 2: 表示应用已经设置了
ctx, hctx
,可以继续处理当前收到的ticket,另外需要重新签发一个ticket,该cb会在签发新ticket的时候再被调用一次,这次enc
会被设为1. - 1:
ctx, hctx
已经设置了,可以继续按默认情况处理。 - 0: 表示应用无法处理该ticket,需要进行完整握手或者使用session ID会话恢复机制。
- 小于0: 出错了。
5.2. 设置签发和校验ticket时候的用户接口和数据
1 | typedef int (*SSL_CTX_generate_session_ticket_fn)(SSL *s, void *arg); |
gen_cb()
是给应用提供的在生成ticket的时候进行的回调,在回调中应用可以调用SSL_SESSION_set1_ticket_appdata()
在ticket中设置用户数据。gen_cb()
中的参数arg
就是SSL_CTX_set_session_ticket_cb()
中的arg
。
默认情况下,会话恢复的时候,TLS1.2不会再签发新的ticket,TLS1.3每次会话恢复都会签发新的ticket,可以用SSL_CTX_set_tlsext_ticket_key_cb()
改变这个行为。
dec_cb()
是在库尝试解密ticket之后,给应用提供的回调。如果解密成功,ss
中存放的是session,keyname
和keyname_len
是解密ticket使用的秘钥标识,status
是解密是否成功,arg
就是SSL_CTX_set_session_ticket_cb()
中的arg
。该回调被调用的时候,sessionss
还没绑定到SSLs
上。做任何操作前, 都要先检查status
:
SSL_TICKET_EMPTY
: 空的ticket数据,就是CH中发送了空的session_ticket
扩展,表示client支持ticket机制。只在TLS1.2之前使用,TLS1.3没意义。SSL_TICKET_NO_DECRYPT
: 无法解密ticket,没有ticket数据可用,且应该给client
发送新的ticket。SSL_TICKET_SUCCESS
: ticket解密成功,可以使用应用数据,不应该发送新的ticket。SSL_TICKET_SUCCESS_RENEW
: 跟SSL_TICKET_SUCCESS
一样,但要发送新的ticket。
dec_cb()
的返回值可以是:
SSL_TICKET_RETURN_ABORT
: 应用判断需要中止握手,可能是由于检测ticket相关数据失败了。注意,TLS1.3一次握手可能会签发多个ticket,一个ticket失效不代表其他也失效,需要小心使用该返回值。SSL_TICKET_RETURN_IGNORE
: 不使用该ticket,也不要签发新的ticket。SSL_TICKET_RETURN_IGNORE_RENEW
:不使用该ticket,但签发一个新的ticket。SSL_TICKET_RETURN_USE
: 使用ticket,但不签发新的ticket。SSL_TICKET_RETURN_USE_RENEW
: 使用ticket,签发新的ticket。
1 | int SSL_SESSION_set1_ticket_appdata(SSL_SESSION *ss, const void *data, size_t len); |
SSL_SESSION_set1_ticket_appdata(()
可以将用户数据存入ticket发送给client。
5.3. 设置TLS1.3中ticket签发个数
1 | int SSL_set_num_tickets(SSL *s, size_t num_tickets); |
设置TLS1.3完整握手后,server可以发送多少个ticket。默认是2个,也可以是0个。会话恢复之后默认发送1个新的ticket,会话恢复后发送的个数不能用这些函数改变,可以用 SSL_CTX_set_tlsext_ticket_key_cb
改变。
TLS1.3中,server使用SSL_verify_client_post_handshake()
进行握手后认证,client发来证书后,还会签发新的ticket,这个ticket个数跟开始的握手时签发的个数一样,如果开始的是完整握手,那也可以在调用SSL_verify_client_post_handshake()
之前调用SSL_set_num_tickets
重新设置签发个数。
参考: