hxndg
V2EX  ›  SSL

PSK_binder, PSK AND TICKET。

  •  
  •   hxndg · Jul 17, 2018 · 2371 views
  •   You need to sign in to view this topic
    This topic created in 2901 days ago, the information mentioned may be changed or developed.

    客户端和服务端通过(EC)DHE 的方式建立了连接,当握手完成之后,也就是服务端收到客户端的 FINISHED 报文之后,服务端可以发送多条 NST 报文,该消息将一个 ticket 键值和从重用主密钥导出的 PSK 建立了关联。

    NST 报文可以一次发送多条,也可以捕获特定事件之后再发送,比方说服务端发起一条 post-handshake 认证报文。   看一下 NST 报文的格式:

    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 用 32bit 网络字节序 unsigned 整型表示。服务端绝对不能使该值大于 604800,也就是7天。该值为0,表示应当立刻丢弃。客户端不应当缓存 lifetime 超过七天的 ticket,服务端对该 ticket 的有效期应当小于 ticket_lifetime。

    ticket_age_add  一个满足随机性,安全生成的 32bit 数值,该数值用于混淆客户端发送过来的 ticket_age。客户端将 ticket_age 加上该值,然后对 2^32 取余。服务端对于每个 ticket 必须重新生成一个新的 ticket_age_add。

    ticket_nonce  独一无二的 ticket_nonce。

    extensions  目前 NST 报文只允许使用一种拓展,即 max_early_data_size,即发送 0-RTT 数据时,客户端可以发送的最大数量应用报文。只考虑应用数据,也就是明文但是不包含 padding 和报文种类等多余的数据。 ticket   ticket 的键值会被使用为 PSK-identity,ticket 本身是一个含混的标签(这一点很容易理解,对客户端而言他是一个身份),它可以是数据库查找的键值,或者是一个自加密-签名的数值。RFC5077 提供了一种推荐的 ticket 构建方法: 服务端使用两种不同的 key:128bit 的 AES-CBC 加密,和 256bit 的 HMAC-SHA-256

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

    key_name 用于鉴别不同的 key,因此服务端可以轻松鉴别自己签发的不同的 ticket。key_name 应当能够避免碰撞攻击。encrypted_state 使用 AES-CBC 模式同 IV 进行加密,MAC 码使用 HMAC-SHA256 对 key_name 和 IV 进行计算。

    struct {
          ProtocolVersion protocol_version;
          CipherSuite cipher_suite;
          CompressionMethod compression_method;
          opaque master_secret[48];
          ClientIdentity client_identity;
          uint32 timestamp;
      } StatePlaintext;
    

    StatePlaintext 存储着包含 master_secret 的 TLS 会话状态,服务端利用时间戳来判断会话状态的过期,client_identity 字段包含认证方式和秘钥交换算法,具体实现的时候可以根据需求来选择代码进行实现。我个人考虑 TLS1.3 不需要提供 certificate_based 的 session 存储,我们只需要提供 psk 的存储即可。值得注意的是,下面的 psk_identity 并不是 TLS1.3 的 psk_identity。

    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;
    

    同当前 ticket 相关联的 PSK 计算方式如下: HKDF-Expand-Label(resumption_master_secret, "resumption", ticket_nonce, Hash.length) ticket_nonce 的独特性保证了 PSK 的独特性。

    服务端通过 NST 报文,将 ticket 发送过来。现在客户端进行第二次连接,客户端需要发送 CH 报文来发送自己的 PSK 信息,CH 报文必须包含 pre_shared_key 拓展,该拓展的格式如下: 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 对应于我们上文定义的 ticket,也可以是外部方式建立的一个标签。    obfuscated_ticket_age 对应于上文中我们说的加了混淆之后的 ticket_age,对应的元素可以在服务端发送的 NST 报文当中找到。如果该 PSK 身份不是通过 NST 报文建立的,那么这个字段必须为 0。    psk_binder 是关键信息,用来将一个 PSK 和当前的握手信息,与上一次通过 NST 报文建立的握手信息创建关联。使用 HMAC 对部分 CLIENTHELLO 报文- CLIENTHELLO 报文从头到 PreSharedKeyExtension.identities (即不包含 binders 字段,因此我们知道 PreSharedKeyExtension 必须是最后的拓展)进行计算,得出 binder。我在这里对于 HMAC 输出的长度存疑,我看到 PskBinderEntry 的长度为 49×8 位,但是我知道的 SHA-384、SHA-256、SHA-1 都没有这么长的输出长度。

    binder_entry_key =
       HKDF-Expand-Label(Binder_Key, "finished", "", Hash.length)
     verify_data =
          HMAC(binder_entry_key,
               Transcript-Hash(Truncate(ClientHello1)))
    

    另外一点需要注意的是如果服务端和客户端之间发送过 HelloRetryEquest,那么第二次计算的内容应当为: Transcript-Hash(ClientHello1, HelloRetryRequest, Truncate(ClientHello2))

    对于服务端,回复的 PSK 拓展,为从 0 开始的选择的客户端的 PSK 序号。
    
    No Comments Yet
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   4948 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 35ms · UTC 09:39 · PVG 17:39 · LAX 02:39 · JFK 05:39
    ♥ Do have faith in what you're doing.