PSK_binder, PSK AND TICKET。

2018-07-17 14:53:05 +08:00
 hxndg

客户端和服务端通过(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 序号。
1811 次点击
所在节点    SSL
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/471627

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX