Skip to content

[DTLS 1.3] Refactor TLS::Cipher_State for TLS/DTLS polymorphism - #6016

Open
reneme wants to merge 3 commits into
randombit:masterfrom
Rohde-Schwarz:dtls13/cipher_state
Open

reneme wants to merge 3 commits into
randombit:masterfrom
Rohde-Schwarz:dtls13/cipher_state

Conversation

@reneme

@reneme reneme commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

This makes TLS::Cipher_State an abstract base class that implements TLS' key schedule and provides low-level building blocks for record (de)protection. The base class is derived by TLS::TLS_Cipher_State that adds TLS-specific record (de)protection based on that.

Also, this patch introduces an Epoch struct that holds the unidirectional cryptographic infrastructure for generations of send/recv traffic keys. Naturally TLS_Cipher_State just handles singleton Epochs, but the upcoming DTLS_Cipher_State (#5000) will maintain historic Epochs for handling retransmissions and out-of-order record delivery.

The agents did a decent job to split this up into reviewable commits:

  1. Splitting out TLS_Cipher_State. The *.cpp has some large hunks, but that's essentially just moving code around.
  2. Introduction of the Epoch structure
  3. Internal refactoring of the record (de)protection to make parts of it reusable for DTLS.

@reneme
reneme requested review from randombit and a balanced review from Copilot October 7, 2026 15:25
@reneme reneme self-assigned this Oct 7, 2026

This comment was marked as resolved.

@reneme
reneme force-pushed the dtls13/cipher_state branch 5 times, most recently from 23f7ad1 to b78a90e Compare October 8, 2026 14:40
@reneme reneme mentioned this pull request Oct 8, 2026
@reneme
reneme force-pushed the dtls13/cipher_state branch 3 times, most recently from fdf7c50 to a55f4f3 Compare October 8, 2026 20:11
reneme and others added 3 commits October 9, 2026 07:43
Cipher_State becomes a (non-instantiable) base class that implements the
(D)TLS 1.3 key schedule (RFC 9846 7.1). Record protection is moved into
a new TLS_Cipher_State subclass.

The base class's public API remains unchanged apart from hosting neither
protect_record() nor deprotect_record() anymore. Those are provided by
the concrete implementations, currently TLS only.

Co-Authored-By: Amos Treiber <amos.treiber@rohde-schwarz.com>
All cryptographic aspects that belong to one generation of traffic keys
(AEAD instance, IV, sequence number, traffic secret and, for handshake
epochs, the Finished key) are now captured in the internal structure
Cipher_State::Epoch. Previously, those were individual members of
Cipher_State.

The base class now accesses the epochs through a few virtual methods and
leaves their management to the concrete subclasses. TLS_Cipher_State
keeps one current epoch per direction and simply rolls it over whenever
new traffic secrets are derived.

Co-Authored-By: Amos Treiber <amos.treiber@rohde-schwarz.com>
The low-level aspects of protecting and deprotecting a record are moved
out of TLS_Cipher_State::protect_record() and ::deprotect_record() into
protected static helpers of Cipher_State that operate on a given Epoch.

This is done to allow re-using these aspects in an upcoming DTLS
variant.

Co-Authored-By: Amos Treiber <amos.treiber@rohde-schwarz.com>
@reneme
reneme force-pushed the dtls13/cipher_state branch from a55f4f3 to 73b74c8 Compare October 9, 2026 05:44

const auto hkdf_label =
concat<secure_vector<uint8_t>>(store_be(static_cast<uint16_t>(length)),
store_be(static_cast<uint8_t>(m_expansion_label_prefix.size() + label.size())),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This drops the check on the label lengths (was BOTAN_ARG_CHECK(prefix.size() + label.size() <= 255, "label too large");)

}

uint64_t Cipher_State::records_decrypted_with_current_key() const {
return latest_read_epoch().sequence_number;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this causes a half-closed connection to fail; peer sends close_notify alert, we remove the read epoch, then the send path checks this value [to see if it should try to request a peer KeyUpdate] which then fails with an exception.

std::optional<Epoch> m_read_epoch;
};

inline TLS_Cipher_State* as_tls_cipher_state(Cipher_State* cs) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces a dynamic cast call per record and I'm not sure there is any need for it - couldn't the record layer just hold the correct type directly?

}

BOTAN_ASSERT_NOMSG((m_encrypt == nullptr) == (m_decrypt == nullptr));
BOTAN_ASSERT_NOMSG(has_write_epoch() == has_read_epoch());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is analagous to the earlier assert, but different in the case of a half-close

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants