0%

ticket相关

1. 为什么

ticket给TLS提供一个不需要在server存储会话状态(session)的机制来恢复会话。适用于TLS1.0, 1.1, 1.2。在以下情况下很有用:

  1. server需要处理大量不同用户的session;
  2. server希望长时间存储session;
  3. 需要使用跨server的负载均衡;
  4. 在内存很少的嵌入式的server上。

之前使用session ID的方法需要server将各种加密参数保存在本地,client下次希望恢复session的时候,就将session ID一并带上,server根据session ID查找本地缓存,找到对应的参数,然后直接建立握手。对照上面几个情况,看session ID为什么不适用:

  1. server处理大量会话需要在本地保存很多加密参数,内存占用和查找效率都会受影响;
  2. 长时间存储session导致长时间占用内存;
  3. session通常保存在一个server上,如果跨server负载均衡,需要将session统一存储,或支持跨server查询;
  4. 内存占用。

2. 是什么

2.1. 主要流程

  1. 第一次完整握手,client在ClientHello中发送一个空的session_ticket扩展,表示client支持ticket会话恢复机制,
  2. server回复一个空的session_ticket扩展,表示自己将会发送一个新的ticket。server会在发送CCS之前发送一个NewSessionTicket的消息,里边存放着用于会话恢复的各种参数,并加密。
  3. client收到后将ticket和本地的session一并存储。
  4. 第二次会话恢复,client在ClientHello中发送不为空的session_ticket扩展,表示client希望进行会话恢复;
  5. server收到后,解密校验,如果校验正确,就使用该session参数进行会话恢复。
  6. 不管server接受不接受client发送的ticket,只要server觉得需要签发一个新的ticket,都要发送session_ticket扩展,也就是说,ServerHello中的session_ticket就是指示server是否发送NewSessionTicket的。

server签发ticket的过程如下:ticket签发

2.2. ticket的生命周期

开始:

  1. client从server接收新签发的ticket,保存到本地。

结束:

  1. ticket在client的缓存中超时;
  2. ticket由client的本地策略提前结束使用;
  3. server新签发了ticket,需要更新本地的ticket;
  4. 会话恢复时出错的时,需要删除ticket。

2.3. ticket的相关结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct {
HandshakeType msg_type;
uint24 length;
select (HandshakeType) {
case hello_request: HelloRequest;
case client_hello: ClientHello;
case server_hello: ServerHello;
case certificate: Certificate;
case server_key_exchange: ServerKeyExchange;
case certificate_request: CertificateRequest;
case server_hello_done: ServerHelloDone;
case certificate_verify: CertificateVerify;
case client_key_exchange: ClientKeyExchange;
case finished: Finished;
case session_ticket: NewSessionTicket; /* NEW */
} body;
} Handshake;

struct {
uint32 ticket_lifetime_hint;
opaque ticket<0..2^16-1>;
} NewSessionTicket;

struct {
opaque key_name[16];
opaque iv[16];
opaque encrypted_state<0..2^16-1>;
opaque mac[32];
} ticket;

其中:

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct {
ProtocolVersion protocol_version;
CipherSuite cipher_suite;
CompressionMethod compression_method;
opaque master_secret[48];
ClientIdentity client_identity;
uint32 timestamp;
} StatePlaintext;

enum {
anonymous(0),
certificate_based(1),
psk(2)
} ClientAuthenticationType;

struct {
ClientAuthenticationType client_authentication_type;
select (ClientAuthenticationType) {
case anonymous: struct {};
case certificate_based:
ASN.1Cert certificate_list<0..2^24-1>;
case psk:
opaque psk_identity<0..2^16-1>; /* from [RFC4279] */
};
} ClientIdentity;

其中:

  • 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机制的交互使用

  1. 第一次握手,如果server表示会签发新的ticket,session ID就为空;
  2. 第二次握手,server拒绝使用ticket,可以发送非空的session ID表示自己支持有状态的会话恢复;
  3. client收到NewSessionTicket的话,就丢掉任何从ServerHello中获取的session ID;
  4. 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是否为空,来判断是否进行会话恢复。
  5. client发送ticket的时候,server禁止使用session ID进行有状态的恢复;
  6. 总结起来就是:有限使用ticket机制。如果client自己生成了session ID,同时又发送了ticket,server也表示要用该ticket恢复,就返回同样的session ID。

3. 安全性

  1. 恢复失败的session会同时将ticket失效。
  2. 被盗的ticket也无所谓,因为ticket是加密的
  3. 伪造的ticket会导致握手失败
  4. DoS攻击的话,使用key_name可以抵御一部分,另外对ticket的解密校验尽量轻量化,比如使用对称加密算法。
  5. 加密ticket的key的管理建议:
    1. key需要随机生成;
    2. key最少128位;
    3. key除了加密和校验ticket,不能用作他用;
    4. key应该周期性更新;
    5. ticket格式或加密算法更新的话,key需要同时更新。
  6. 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. 主要流程

  1. 第一次握手,server如果支持会话恢复,就在握手完成后,发送NewSessionTicket消息;
  2. client如果收到NewSessionTicket消息,就将当前session和其中的ticket存储到一起;
  3. 第二次握手,client在CH中发送pre_shared_key扩展项,其中携带ticket。
  4. server收到psk扩展后,先解密校验ticket,然后用ticket中的PSK相关key去校验psk。
  5. server在握手完成后,还可能再签发一个新的NewSessionTicket用于替换之前的。

4.2. 消息格式

4.2.1. server新签发的ticket的NewSessionTicket的消息格式为:

1
2
3
4
5
6
7
struct {
uint32 ticket_lifetime;
uint32 ticket_age_add;
opaque ticket_nonce<0..255>;
opaque ticket<1..2^16-1>;
Extension extensions<0..2^16-2>;
} NewSessionTicket;

其中:

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct {
opaque identity<1..2^16-1>;
uint32 obfuscated_ticket_age;
} PskIdentity;

opaque PskBinderEntry<32..255>;

struct {
PskIdentity identities<7..2^16-1>;
PskBinderEntry binders<33..2^16-1>;
} OfferedPsks;

struct {
select (Handshake.msg_type) {
case client_hello: OfferedPsks;
case server_hello: uint16 selected_identity;
};
} PreSharedKeyExtension;

其中:

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
#include <openssl/tls1.h>

int SSL_CTX_set_tlsext_ticket_key_evp_cb(SSL_CTX sslctx,
int (*cb)(SSL *s, unsigned char key_name[16],
unsigned char iv[EVP_MAX_IV_LENGTH],
EVP_CIPHER_CTX *ctx, EVP_MAC_CTX *hctx, int enc));
// OpenSSL 3.0引入

int SSL_CTX_set_tlsext_ticket_key_cb(SSL_CTX sslctx,
int (*cb)(SSL *s, unsigned char key_name[16],
unsigned char iv[EVP_MAX_IV_LENGTH],
EVP_CIPHER_CTX *ctx, HMAC_CTX *hctx, int enc));
// OpenSSL 3.0中被废除

因为没有必要为每一个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
2
3
4
5
typedef int (*SSL_CTX_generate_session_ticket_fn)(SSL *s, void *arg);

typedef SSL_TICKET_RETURN (*SSL_CTX_decrypt_session_ticket_fn)(SSL *s, SSL_SESSION *ss, const unsigned char *keyname, size_t keyname_len, SSL_TICKET_STATUS status, void *arg);

int SSL_CTX_set_session_ticket_cb(SSL_CTX *ctx, SSL_CTX_generate_session_ticket_fn gen_cb, SSL_CTX_decrypt_session_ticket_fn dec_cb, 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,keynamekeyname_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
2
3
int SSL_SESSION_set1_ticket_appdata(SSL_SESSION *ss, const void *data, size_t len);

int SSL_SESSION_get0_ticket_appdata(SSL_SESSION *ss, void **data, size_t *len);

SSL_SESSION_set1_ticket_appdata(()可以将用户数据存入ticket发送给client。

5.3. 设置TLS1.3中ticket签发个数

1
2
3
4
int SSL_set_num_tickets(SSL *s, size_t num_tickets);
size_t SSL_get_num_tickets(SSL *s);
int SSL_CTX_set_num_tickets(SSL_CTX *ctx, size_t num_tickets);
size_t SSL_CTX_get_num_tickets(SSL_CTX *ctx);

设置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重新设置签发个数。

参考:

  1. openssl手册
  2. RFC8446-TLS1.3 PSK,ticket
  3. RFC5246-TLS1.2 session ID
  4. RFC5077-Transport Layer Security (TLS) Session Resumption without
    Server-Side State
    )