Fix transcript hash for DTLS 1.3. The DTLS 1.3 handshake transcript uses the same format as TLS 1.3, i.e. without the message_seq, fragment_offset, and fragment_length values from the DTLSHandshake struct. Change-Id: Ic46ee5519d92d15b194a47149e4497c18be52876 Bug: 42290594 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/71947 Reviewed-by: Adam Langley <agl@google.com> Commit-Queue: Nick Harper <nharper@chromium.org> Auto-Submit: Nick Harper <nharper@chromium.org> Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/ssl/handshake.cc b/ssl/handshake.cc index 7195d66..0c5895f 100644 --- a/ssl/handshake.cc +++ b/ssl/handshake.cc
@@ -126,6 +126,8 @@ SSL_HANDSHAKE::SSL_HANDSHAKE(SSL *ssl_arg) : ssl(ssl_arg), + transcript(SSL_is_dtls(ssl_arg)), + inner_transcript(SSL_is_dtls(ssl_arg)), ech_is_inner(false), ech_authenticated_reject(false), scts_requested(false),
diff --git a/ssl/internal.h b/ssl/internal.h index a3b1ce0..dc539a9 100644 --- a/ssl/internal.h +++ b/ssl/internal.h
@@ -937,7 +937,7 @@ // buffer and running hash. class SSLTranscript { public: - SSLTranscript(); + explicit SSLTranscript(bool is_dtls); ~SSLTranscript(); SSLTranscript(SSLTranscript &&other) = default; @@ -1000,10 +1000,23 @@ bool from_server) const; private: + // HashBuffer initializes |ctx| to use |digest| and writes the contents of + // |buffer_| to |ctx|. If this SSLTranscript is for DTLS 1.3, the appropriate + // bytes in |buffer_| will be skipped when hashing the buffer. + bool HashBuffer(EVP_MD_CTX *ctx, const EVP_MD *digest) const; + + // AddToBufferOrHash directly adds the contents of |in| to |buffer_| and/or + // |hash_|. + bool AddToBufferOrHash(Span<const uint8_t> in); + // buffer_, if non-null, contains the handshake transcript. UniquePtr<BUF_MEM> buffer_; // hash, if initialized with an |EVP_MD|, maintains the handshake hash. ScopedEVP_MD_CTX hash_; + // is_dtls_ indicates whether this is a transcript for a DTLS connection. + bool is_dtls_ : 1; + // version_ contains the version for the connection (if known). + uint16_t version_ = 0; }; // tls1_prf computes the PRF function for |ssl|. It fills |out|, using |secret|
diff --git a/ssl/ssl_transcript.cc b/ssl/ssl_transcript.cc index 239363d..4d813df 100644 --- a/ssl/ssl_transcript.cc +++ b/ssl/ssl_transcript.cc
@@ -144,7 +144,7 @@ BSSL_NAMESPACE_BEGIN -SSLTranscript::SSLTranscript() {} +SSLTranscript::SSLTranscript(bool is_dtls) : is_dtls_(is_dtls) {} SSLTranscript::~SSLTranscript() {} @@ -159,13 +159,73 @@ } bool SSLTranscript::InitHash(uint16_t version, const SSL_CIPHER *cipher) { + version_ = version; const EVP_MD *md = ssl_get_handshake_digest(version, cipher); if (Digest() == md) { // No need to re-hash the buffer. return true; } - return EVP_DigestInit_ex(hash_.get(), md, nullptr) && - EVP_DigestUpdate(hash_.get(), buffer_->data, buffer_->length); + if (!HashBuffer(hash_.get(), md)) { + return false; + } + if (is_dtls_ && version_ >= TLS1_3_VERSION) { + // In DTLS 1.3, prior to the call to InitHash, the message (if present) in + // the buffer has the DTLS 1.2 header. After the call to InitHash, the TLS + // 1.3 header is written by SSLTranscript::Update. If the buffer isn't freed + // here, it would have a mix of different header formats and using it would + // yield wrong results. However, there's no need for the buffer once the + // version and the digest for the cipher suite are known, so the buffer is + // freed here to avoid potential misuse of the SSLTranscript object. + FreeBuffer(); + } + return true; +} + +bool SSLTranscript::HashBuffer(EVP_MD_CTX *ctx, const EVP_MD *digest) const { + if (!EVP_DigestInit_ex(ctx, digest, nullptr)) { + return false; + } + if (!is_dtls_ || version_ < TLS1_3_VERSION) { + return EVP_DigestUpdate(ctx, buffer_->data, buffer_->length); + } + + // If the version is DTLS 1.3 and we still have a buffer, then there should be + // at most a single DTLSHandshake message in the buffer, for the ClientHello. + // On the server side, the version (DTLS 1.3) and cipher suite are chosen in + // response to the first ClientHello, and InitHash is called before that + // ClientHello is added to the SSLTranscript, so the buffer is empty if this + // SSLTranscript is on the server. + if (buffer_->length == 0) { + return true; + } + + // On the client side, we can receive either a ServerHello or + // HelloRetryRequest in response to the ClientHello. Regardless of which + // message we receive, the client code calls InitHash before updating the + // transcript with that message, so the ClientHello is the only message in the + // buffer. In DTLS 1.3, we need to skip the message_seq, fragment_offset, and + // fragment_length fields from the DTLSHandshake message in the buffer. The + // structure of a DTLSHandshake message is as follows (RFC 9147, section 5.2): + // + // struct { + // HandshakeType msg_type; /* handshake type */ + // uint24 length; /* bytes in message */ + // uint16 message_seq; /* DTLS-required field */ + // uint24 fragment_offset; /* DTLS-required field */ + // uint24 fragment_length; /* DTLS-required field */ + // select (msg_type) { + // /* omitted for brevity */ + // } body; + // } DTLSHandshake; + CBS buf, header; + CBS_init(&buf, reinterpret_cast<uint8_t *>(buffer_->data), buffer_->length); + if (!CBS_get_bytes(&buf, &header, 4) || // + !CBS_skip(&buf, 8) || // + !EVP_DigestUpdate(ctx, CBS_data(&header), CBS_len(&header)) || // + !EVP_DigestUpdate(ctx, CBS_data(&buf), CBS_len(&buf))) { + return false; + } + return true; } void SSLTranscript::FreeBuffer() { @@ -193,8 +253,8 @@ const uint8_t header[4] = {SSL3_MT_MESSAGE_HASH, 0, 0, static_cast<uint8_t>(hash_len)}; if (!EVP_DigestInit_ex(hash_.get(), Digest(), nullptr) || - !Update(header) || - !Update(MakeConstSpan(old_hash, hash_len))) { + !AddToBufferOrHash(header) || + !AddToBufferOrHash(MakeConstSpan(old_hash, hash_len))) { return false; } return true; @@ -209,8 +269,7 @@ } if (buffer_) { - return EVP_DigestInit_ex(ctx, digest, nullptr) && - EVP_DigestUpdate(ctx, buffer_->data, buffer_->length); + return HashBuffer(ctx, digest); } OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR); @@ -218,6 +277,27 @@ } bool SSLTranscript::Update(Span<const uint8_t> in) { + if (!is_dtls_ || version_ < TLS1_3_VERSION) { + return AddToBufferOrHash(in); + } + if (in.size() < DTLS1_HM_HEADER_LENGTH) { + return false; + } + // The message passed into Update is the whole Handshake or DTLSHandshake + // message, including the msg_type and length. In DTLS, the DTLSHandshake + // message also has message_seq, fragment_offset, and fragment_length + // fields. In DTLS 1.3, those fields are omitted so that the same + // transcript format as TLS 1.3 is used. This means we write the 1-byte + // msg_type, 3-byte length, then skip 2+3+3 bytes for the DTLS-specific + // fields that get omitted. + if (!AddToBufferOrHash(in.subspan(0, 4)) || + !AddToBufferOrHash(in.subspan(12))) { + return false; + } + return true; +} + +bool SSLTranscript::AddToBufferOrHash(Span<const uint8_t> in) { // Depending on the state of the handshake, either the handshake buffer may be // active, the rolling hash, or both. if (buffer_ &&
diff --git a/ssl/test/runner/prf.go b/ssl/test/runner/prf.go index 3ef23df..0324c87 100644 --- a/ssl/test/runner/prf.go +++ b/ssl/test/runner/prf.go
@@ -258,8 +258,8 @@ // handshake message with a TLS header. In DTLS, the header is rewritten to a // DTLS header with |seqno| as the sequence number. func (h *finishedHash) WriteHandshake(msg []byte, seqno uint16) { - if h.isDTLS { - // This is somewhat hacky. DTLS hashes a slightly different format. + if h.isDTLS && h.version <= VersionTLS12 { + // This is somewhat hacky. DTLS <= 1.2 hashes a slightly different format. (DTLS 1.3 uses the same format as TLS.) // First, the TLS header. h.Write(msg[:4]) // Then the sequence number and reassembled fragment offset (always 0).