TLS1.3TicketIdentity设计

TLS1.3中的基础知识

Posted by 大狗 on August 31, 2020

TLS1.3 Ticket Identity设计

前言

很多时候设计数据结构是个令人尴尬的事情,用户量小/只有自己使用,那随便写。但是如果用户量变大了/出现了内存写坏的情况,那DEBUG可以说是要死人了。所以打好一个框架可以说是非常重要的。不过讲道理设计Identity的时候我是参照RFC5077做的,然后吴哥设计的时候也是照5077做的,抄就完事了,就是最后实现identity和原本session的结构关联的时候增删改查四个API改的我那叫一个蛋疼。这里作为基础知识,就不讲Session Identity和Session Id的区别了,以后再写了。

Ticket Identity设计流程

TLS1.3 Ticket Identity的设计目标

与TLS1.2不同,TLS1.3设计的时候我们考虑了不少别的东西。很多规定并不是强制要求的,但是出于安全性考虑还是实现了

  • Ticket Identity应当减少服务端的运算/内存占用,避免非对称操作出现。
  • Ticket Identity应当满足安全性要求,同时运算够快。
  • Ticket Identity应当能够鉴别客户端视角Identity寿命和服务端Identity寿命差值,留个小问题,如何设定两边差值的大小?
  • Ticket Identity应当包含ServerName信息,从而校验复用时ServerName是否一致。
  • Ticket Identity应当包含使用的Cipher信息,从而校验复用时Cipher是否一致。
  • Ticket Identity应当包含版本,ticket_age_add信息。

上面的几条要求,除了第一条和第二条剩下都是“需要保存xxx”,这些存储明文即可。Ticket Identity减少内存占用,那就不在服务端保存任何消息。同时要符合安全性和少运算的要求,那么就是用hmac和ase做加密操作。

TLS1.3 Ticket Identity明文部分的设计方案

明文部分设计方案可以说是非常简单了,关键信息一个不能少,也不能多到那里去。

	struct {
          ProtocolVersion protocol_version;		//版本信息
          CipherSuite cipher_suite;				//cipher信息
          opaque ticket_age_add;				//age_add信息
		  opaque ticket_birth;					//birth信息
          opaque secret;						//啥secret自己看着办
          opaque servername;					//servername信息
      } IdentityPlaintext;

TLS1.3 Ticket Identity安全性方面的设计方案

安全性方面目标就两个,一个是保密性,另一个就是完整性

  • 保密性:保密性实际上就是要加密IdentityPlaintext,不能从明文透露Identity信息。但是不能使用非对称加密,因为非对称太消耗时间和性能了,所以使用AES比较合适。
  • 完整性:完整性本质就是防篡改,加个hmac就成了。 这里留下来一个问题,无论是AES还是HMAC都是需要使用密钥的,这个密钥如何产生,多久改变一次?

TLS1.3 Ticket Identity的数据结构查找方面的设计

因为作为Server时,不需要保存Ticket Identity的信息,所以只需要保存作为client端的Ticket Identity信息即可。那么如何保存Ticket Identity呢?只简单写写设计原则

  • 一方面要跨线程方便查找,另一方面要能快速找到。所以一方面哈希表,另一方面二叉树
  • 锁大了不好,锁小了好,实在不行per线程自己保存完事了,不会出现任何碰撞最简单。
  • 结果最好和原先兼容,能挂到一起最好

OPENSSL如何设计Ticket Identity的?

OPENSSL的Ticket Identity和我司的设计并不一样,比方说OPENSSL会有ANTI-REPLAY举措,他们会保存Ticket Identity,所以不需要生成完全无状态ticket。 我们来看OPENSSL生成无状态Ticket时是如何存储的。这里就直接看ssl_session_st了,不多分析流程了。

	int i2d_SSL_SESSION(const SSL_SESSION *in, unsigned char **pp)
{

    SSL_SESSION_ASN1 as;

    ASN1_OCTET_STRING cipher;
    unsigned char cipher_data[2];
    ASN1_OCTET_STRING master_key, session_id, sid_ctx;

#ifndef OPENSSL_NO_COMP
    ASN1_OCTET_STRING comp_id;
    unsigned char comp_id_data;
#endif
    ASN1_OCTET_STRING tlsext_hostname, tlsext_tick;
#ifndef OPENSSL_NO_SRP
    ASN1_OCTET_STRING srp_username;
#endif
#ifndef OPENSSL_NO_PSK
    ASN1_OCTET_STRING psk_identity, psk_identity_hint;
#endif
    ASN1_OCTET_STRING alpn_selected;
    ASN1_OCTET_STRING ticket_appdata;

    long l;

    if ((in == NULL) || ((in->cipher == NULL) && (in->cipher_id == 0)))
        return 0;

    memset(&as, 0, sizeof(as));

    as.version = SSL_SESSION_ASN1_VERSION;
    as.ssl_version = in->ssl_version;

    if (in->cipher == NULL)
        l = in->cipher_id;
    else
        l = in->cipher->id;
    cipher_data[0] = ((unsigned char)(l >> 8L)) & 0xff;
    cipher_data[1] = ((unsigned char)(l)) & 0xff;

    ssl_session_oinit(&as.cipher, &cipher, cipher_data, 2);

#ifndef OPENSSL_NO_COMP
    if (in->compress_meth) {
        comp_id_data = (unsigned char)in->compress_meth;
        ssl_session_oinit(&as.comp_id, &comp_id, &comp_id_data, 1);
    }
#endif

    ssl_session_oinit(&as.master_key, &master_key,
                      in->master_key, in->master_key_length);

    ssl_session_oinit(&as.session_id, &session_id,
                      in->session_id, in->session_id_length);

    ssl_session_oinit(&as.session_id_context, &sid_ctx,
                      in->sid_ctx, in->sid_ctx_length);

    as.time = in->time;
    as.timeout = in->timeout;
    as.verify_result = in->verify_result;

    as.peer = in->peer;

    ssl_session_sinit(&as.tlsext_hostname, &tlsext_hostname,
                      in->ext.hostname);
    if (in->ext.tick) {
        ssl_session_oinit(&as.tlsext_tick, &tlsext_tick,
                          in->ext.tick, in->ext.ticklen);
    }
    if (in->ext.tick_lifetime_hint > 0)
        as.tlsext_tick_lifetime_hint = in->ext.tick_lifetime_hint;
    as.tlsext_tick_age_add = in->ext.tick_age_add;
#ifndef OPENSSL_NO_PSK
    ssl_session_sinit(&as.psk_identity_hint, &psk_identity_hint,
                      in->psk_identity_hint);
    ssl_session_sinit(&as.psk_identity, &psk_identity, in->psk_identity);
#endif                          /* OPENSSL_NO_PSK */
#ifndef OPENSSL_NO_SRP
    ssl_session_sinit(&as.srp_username, &srp_username, in->srp_username);
#endif                          /* OPENSSL_NO_SRP */

    as.flags = in->flags;
    as.max_early_data = in->ext.max_early_data;

    if (in->ext.alpn_selected == NULL)
        as.alpn_selected = NULL;
    else
        ssl_session_oinit(&as.alpn_selected, &alpn_selected,
                          in->ext.alpn_selected, in->ext.alpn_selected_len);

    as.tlsext_max_fragment_len_mode = in->ext.max_fragment_len_mode;

    if (in->ticket_appdata == NULL)
        as.ticket_appdata = NULL;
    else
        ssl_session_oinit(&as.ticket_appdata, &ticket_appdata,
                          in->ticket_appdata, in->ticket_appdata_len);

    return i2d_SSL_SESSION_ASN1(&as, pp);

}

上面是创建基础元,创建完成之后使用AES-256-CBC+HSA256进行ticket的完整运算,最后写入到数据包中。

比较OPENSSL和我们设计的Ticket Identity

可以看到,OPENSSL实现的时候和我司的实现查别还是很大的。两者都是遵循最小信息元原则。使用的加密算法和完整性算法也是类似的。明显都是照着RFC5077做的嘛。但OPENSSL如果不保存Ticket Identity,那么它使用的Ticket Hmac Key是写死的,这点比较糟糕。那么问题来了,AES Key和HMAC Key应当如何更新,多久更新一次?我个人建议是和Ticket Identity的日期保持一致,这东西实际上跟环境还是有关系的,必将算个hmac和aes也不算慢,不会是瓶颈。

结尾的闲言碎语

写到这里差不多就可以结束了,实际上Session Identity还经常需要添加ALPN信息,就不多说了。TLS这块还有啥不明白的直接告诉我就成了 狗头的赞赏码.jpg