Hi folks, There have been a lot of discussions about whether we should try to refactor cipher-suite negotiation to be less monolithic in the cipher suite. I've generally been on the "no" side of that on cost/benefit grounds as well as not quite seeing how it would fit into the rest of the infrastructure. However, now that we're starting to get full implementations, and it's becoming clearer how the new elements (principally PSK-resumption and key_shares) interact with cipher suites, I've started to think that in fact we may be able to clean things up. One proposal for this is below [0]. I know this is pretty late in the process, but if we do want this change it's better to do it now.
I'm sure we'll need to discuss this in Berlin, but in the meantime, fire away. The basic idea here is to factor out the TLS 1.3 negotiation into three mostly orthogonal axes. - Symmetric cipher/PRF -- indicated by the cipher suite list as in TLS 1.2 - Key exchange -- indicated by the key_shares and pre_shared_key extensions - Authentication -- indicated the signature_algorithms and pre_shared_key extensions A proposal for how to do this is below. See the end for other options, caveats, etc. If we take PSK out of the picture, this gives us a very simple structure: - The client offers a set of key_shares and the server picks one that it likes [1]. - The client offers a list of signature_algorithms and the server picks a certificate/key that matches that list and signs with it. In other words, we just eliminate the redundancy with the cipher suite indications. This leaves is with the question of how to handle the existing TLS 1.2 cipher suites. We can either assign new cipher suites or say that any cipher suite with *_aead_alg_hash means that we support aead_alg_hash. Matter of taste. PSK is handled by extending the concept of PSK flags that we already have in NewSessionTicket to also include uses of PSKs where you indicate the way in which you are using the PSK (or want it to be used). There a bunch of ways to encode this. I'll give you the one I mostly prefer below and then a one that's a smaller change but I think a little less elegant at the end [note #4]. First, we replace the flags word of the different KE modes for the ticket with lists of code points, as below: enum { psk_ke(0), // PSK key exchange psk_dhe_ke(1), // PSK + DHE key exchange (255) } PskKeModes; And then add new code points for being able to use the PSK with and without signatures (from the server). We had pretty rough consensus in B-A that we needed this mode and [draft-thomson-tls-0rtt-and-certs-01] is part of the motivation for this idea. enum { psk_auth(0), // PSK only psk_sign_auth(1), // PSK + a signature (as in draft-thomson) [5] (255) } PskAuthModes; This gives us the following NewSessionTicket message where we have replaced the flags word with two (potentially ordered) lists: struct { uint32 ticket_lifetime; PskAuthModes auth_modes<1..255>; PskKeModes ke_modes<1..255>; TicketExtension extensions<2..2^16-2>; opaque ticket<0..2^16-1>; } NewSessionTicket; We now need a way to indicate that the server will do 0-RTT (previously in the flags word) so we add a new 0-RTT extension: struct { uint32 ticket_age_add; } TicketEarlyDataInfo; This has two nice properties: - You don't need to provide ticket_age_add when you don't do 0-RTT (and it is silly if you don't). - It exercises this extension mechanism, which is good for future-proofing. PreSharedKeyExtension becomes: struct { PskAuthMode auth_modes<1..255>; PskKeModee ke_modes<1..255>; opaque identity<0..2^16-1>; } PskIdentity; struct { select (Role) { case client: PskIdentity identities<2..2^16-1>; case server: PskAuthMode auth_mode; PskKeMode ke_mode; uint16 selected_identity; } } PreSharedKeyExtension; The way to interpret these is as follows: - With each identity, the client indicates to the server which modes it can be used with. - When the server responds with an identity, it tells you how it used it. You might ask why you need the server to indicate what it did. The reason is that we would like the client to know in advance (at the time of the ServerHello) whether the server has sent Certificate/CertificateVerify rather than having to figure it out from what messages the server sends. The ke_mode field is redundant (because you also infer it from the server key_shares) but I added it for parity. I haven't implemented this yet (am going to try to take a crack at that before Berlin) but I believe based on experience with the NSS negotiation that it will be simpler. I know it removes a bunch of odd edge cases which have accumulated over the years, but maybe it adds others. -Ekr BONUS MATERIAL 0. I've discussed this with and/or borrowed ideas from Richard Barnes, David Benjamin, Karthik Bhargavan, Dave Garrett, Nick Sullivan, Martin Thomson, and others. Thanks to those guys, but blame me if you think this is bad. 1. The other major encoding option here is to have dummy group/signature_algorithm indicators that tell you whether you can use a pure PSK. When I and others discussed this, the consensus was that that was less clean. 2. One question that comes up at the same time is whether we should allow multiple key shares to be used, which is a structure that this makes pretty easy. Basically, the server would just supply as many counter-shares as it wanted and then we'd need a defined order for how they were inserted into the key schedule. This feature would be nice for enabling post-quantum, but probably better to just define <PQ-Algorithm + Curve> code points. 3. Note that because of this PSK-PRF interaction, PSK isn't totally orthogonal with AEAD/PRF. I.e., you cannot use the same PSK with HKDF-SHA256 and HKDF-SHA384 if you want to be on the cryptographic fairway, but I think it should be easy enough to filter out the cipher suites to make that work. 4. The other major alternative is just to use the flags bits. So we would extend and generalize the flags as shown below. enum { early_data(1), dhe_psk_ke(2), psk_ke(4), psk_auth(8), // New (no server cert) sig_psk_auth(16) // New (sign with server cert) } PskUsageFlags; These new flags have the expected meaning, namely: psk_auth The server will do connections with just PSK authentication (equivalent to PSK now) sig_psk_auth The server will do connections with PSK plus signature (not currently specified). Then we update the PreSharedKeyExtension as follows: struct { uint32 usage_flags; opaque identity<0..2^16-1>; } PskIdentity; struct { select (Role) { case client: PskIdentity identities<2..2^16-1>; case server: uint32 usage_flags; // New (one from each category) uint16 selected_identity; } } PreSharedKeyExtension; The idea here is: - The client indicates how each PSK can be used (by flags, which need to be a subset of the ticket flags). - The server can pick a PSK and indicate how it was actually used. For instance, if the client wants to do PSK w/ ECDHE it would indicate dhe_psk_ke. If it only wants PSK it would indicate psk_ke. If it was willing to do both, it would indicate the bitwise OR. Similarly, if the client wants PSK for auth, it would indicate psk_auth. If it wants the server to sign, it indicates sig_psk_auth, and so on (indicating neither is silly because it makes the key unusable). By contrast, the server just sets the bits that it actually used. I.e., if it sets psk_auth, that means it won't be signing and there is no Certificate/CertificateVerify. OTOH if it sets sign_psk_auth, that means it will be signing and there will be a Certificate/CertificateVerify. This leaves early data. As above, the client uses the early_data bit to indicate that the PSK was used to encrypt early data (obviously, this can just be one key) and the server uses it to indicate that it accepted early data. ticket_age needs to go in its own extension, but that's easy enough. 5. Note: this would allow for modes where the server signs over a PSK handshake with no DHE at all. The resumption_ctx mechanism is intended to ensure that that is OK, but we'd need to confirm with analysis.
_______________________________________________ TLS mailing list TLS@ietf.org https://www.ietf.org/mailman/listinfo/tls