DTLS - Datagram Transport Layer Security

pcapkit.protocols.application.dtls contains DTLS, which implements an extractor for Datagram Transport Layer Security (DTLS) [*] – DTLS 1.0 (RFC 4347), DTLS 1.2 (RFC 6347) and its connection-ID records (RFC 9146), and DTLS 1.3’s legacy-format and unified-header records (RFC 9147) – at the record layer, plus the epoch/sequence-number fields and the handshake-message fragment header that set DTLS apart from TLS, whose reliable, ordered transport needs neither. It also contains DTLSContext, which holds the connection IDs those records carry.

Version Identification

As with TLS, one class parses every version and read() sets alias from the record; id() stays ('DTLS',).

Legacy record version

alias

0xFEFF

DTLS 1.0

0xFEFD

DTLS 1.2

0xFEFD, or 0xFEFF on an initial ClientHello, whose supported_versions extension lists 0xFEFC

DTLS 1.3

any other 0xFExx

DTLS

unified header (first octet 001CSLEE, RFC 9147 Section 4.1)

DTLS 1.3

DTLS 1.3 never puts its own 0xFEFC on the record layer (RFC 9147 Section 4), so a legacy-format record is told apart only by the supported_versions peek. A version the table does not name keeps the generic DTLS alias rather than claiming a specific one.

A unified-header record has no legacy version to read. Its first octet’s top three bits are 001, which no legacy content type (20 through 27) sets, so the two formats are told apart by that octet alone.

A tls12_cid record is DTLS 1.2: RFC 9146 Section 10.3 limits the content type to that version, so one with another legacy version is refused.

Connection IDs

A tls12_cid record carries a connection ID between its sequence number and its length (RFC 9146 Section 4), and a DTLS 1.3 unified header with its C bit set carries one between its first octet and its sequence number (RFC 9147 Section 4). Either way the ID’s length is negotiated rather than carried. DTLSContext resolves it for both, in this order:

  • a connection ID that a ClientHello or ServerHello connection_id extension negotiated earlier in the capture, each peer naming the one it receives (RFC 9146 Section 3, RFC 9147 Section 9.1), and that the record’s ID begins with;

  • the cid_length the caller gives DTLSContext.

Without either the record stays Raw. A dissected one reports its connection ID alongside its other header fields, and keeps its encrypted content as octets. The context reaches DTLS through pcapkit.corekit.context:

import pcapkit
from pcapkit.protocols.application.dtls import DTLSContext

extraction = pcapkit.extract('dtls.pcap', context=DTLSContext(cid_length=8))

The connection IDs a DTLSContext learns belong to that instance. A new instance starts with none, and one the caller reuses keeps what it has learned. An extraction given none gets its own in its own registry, so it carries what its handshakes negotiate across its capture and no further. A registry passed as __context__ to several parses carries its instance across them.

Matching by value relies on IDs being self-delineating (RFC 9146 Section 4): with c1c2c3c4 registered, a record whose ID is c1c2c3c400010203 reads as carrying c1c2c3c4.

A rebuild parses each layer’s octets again, so from_data takes the same __context__. Without it the octets still rebuild exactly, and DTLS.from_data keeps its chain, but a record with a connection ID under a rebuilt UDP layer reads as Raw.

Records in One Datagram

A whole record after the first in a UDP datagram is dissected as a further DTLS layer, the previous record’s payload (RFC 6347 Section 4.1.1, RFC 9147 Section 4.3, #2535): a ChangeCipherSpec followed by an Alert reads UDP:DTLS 1.2:DTLS 1.2. A unified-header record is delimited by its length; one without a length runs to the end of the datagram, so it is the last (RFC 9147 Section 4). The exceptions:

  • A whole record that fails to decode becomes Raw together with everything after it.

  • Octets that open no whole record stay raw, with no error: an unsupported content type, a legacy version outside 0xFExx, a header cut short, a length past the end of the datagram, or a connection ID whose length is not known, which leaves the record’s end unknown.

  • A parse told to stop at DTLS (protocol='DTLS' or layer='Application') keeps the rest of the datagram raw.

  • Each record is a layer, so FRAME_LAYER_LIMIT bounds how many are dissected.

Known Limitations

A handshake message split across DTLS records is not decoded here (RFC 6347 Section 4.2.3, #2440). Each fragment is flagged is_fragmented and its body kept as raw octets. Extractor(reassembly=True, dtls=True) reassembles the message, and dtls_handshake() decodes it with analyze_handshake(), which reads it with read_handshake().

A connection ID negotiated in a fragmented hello is not learned: the message is not decoded here, so its connection_id extension is not read.

A connection ID a DTLS 1.3 NewConnectionId issues is not learned (RFC 9147 Section 9, #2625): the message is encrypted, so only the caller can give it to DTLSContext.

A unified-header record carries only the low two bits of the epoch, and its sequence number is header-protected, so the values it reports are the ones on the wire.

Added in version 1.6.0.


class pcapkit.protocols.application.dtls.DTLS(file=None, length=None, **kwargs)[source]

Bases: SSL[DTLS, DTLS]

This class implements all protocols in the DTLS record-layer family.

  • Datagram Transport Layer Security 1.0 [RFC 4347]

  • Datagram Transport Layer Security 1.2 [RFC 6347], and its connection-ID records [RFC 9146]

  • Datagram Transport Layer Security 1.3, legacy-format and unified-header records [RFC 9147]

Note

The subpackage does not track the dispatch tier: DTLS is dispatched from UDP.__proto__ at port 5684 (coaps, RFC 7252) only, though IANA registers other DTLS-borne services; bind more with pcapkit.foundation.registry.protocols.transport.register_udp().

property name: Literal['Datagram Transport Layer Security']

Name of current protocol.

property alias: Literal['DTLS', 'DTLS 1.0', 'DTLS 1.2', 'DTLS 1.3']

Acronym of current protocol – the concrete tier read() identified.

property length: int

Header length of current protocol – the record header, the fragment excluded. Fixed at 13 octets for a legacy record, and the connection ID’s length more for a tls12_cid one; a unified header’s length follows from its first octet and its connection ID.

classmethod id()[source]

Index ID of the protocol.

Return type:

tuple[Literal['DTLS']]

Returns:

Index ID of the protocol. Fixed, regardless of which tier read() identifies the record as – see TLS.id for why.

read(length=None, **kwargs)[source]

Read Datagram Transport Layer Security (DTLS).

Parameters:
  • length (int | None) – Length of packet data.

  • **kwargs (Any) – Arbitrary keyword arguments.

Return type:

DTLS

Returns:

Parsed packet data.

Raises:

ProtocolError – If the record cannot be a genuine DTLS record – see _read_dtls_record().

make(content_type=ContentType.application_data, version=0xFEFD, epoch=0, sequence_number=0, content=b'', data=None, payload=b'', ciphertext=False, sequence_long=False, length_present=True, cid=None, **kwargs)[source]

Make (construct) packet data, for every tier: version is written as is.

Parameters:
  • content_type (ContentType | int) – Record content type.

  • version (int) – Legacy record version (major << 8 | minor), e.g. 0xFEFD for DTLS 1.2 and (the legacy value DTLS 1.3 keeps here) DTLS 1.3.

  • epoch (int) – Epoch. A unified-header record carries only its low two bits, so a larger value is refused.

  • sequence_number (int) – Sequence number within epoch (48-bit); for a unified-header record, 8 or 16 bits as sequence_long says.

  • content (Any) – Record content: a tuple of DTLSHandshake for handshake, an Alert, a ChangeCipherSpec, or raw bytes, as read() would decode it. For a unified-header or tls12_cid record, the ciphertext as bytes.

  • data (bytes | None) – Pre-encoded record fragment. When given, it is used verbatim in place of content and length is computed from it.

  • payload (bytes | ProtocolBase | Schema) – Octets after the record, e.g. a further record coalesced in the same buffer. The connection IDs of the records a DTLS payload carries are registered, so that its octets parse again (DTLSContext).

  • ciphertext (bool) – Make a unified-header record (RFC 9147 Section 4.1) instead of a legacy one; content_type, version and data do not apply.

  • sequence_long (bool) – Whether a unified-header record’s sequence number is 16 bits rather than 8.

  • length_present (bool) – Whether a unified-header record carries its length; if not, it runs to the end of the datagram.

  • cid (bytes | None) – Connection ID of a tls12_cid record (RFC 9146 Section 4), which needs one, or of a unified-header record, which sets its C bit (RFC 9147 Section 4); 1 to 255 octets. No other record takes one.

  • **kwargs (Any) – Arbitrary keyword arguments.

Return type:

DTLS | DTLSCiphertext | DTLSCIDCiphertext

Returns:

Constructed packet data.

Raises:
  • ProtocolError – If cid is invalid, missing for a tls12_cid record, or given for a legacy-format one of another type; if a legacy-format record’s content_type is a name, a bool, or a member or code the 8-bit content type field cannot carry; or if an extension of content given as a (type, kwargs) pair has such a type for its 16-bit field.

  • StructError – If another record-header field, or a field of content, does not fit its width.

  • FieldValueError – If a unified-header record’s epoch does not fit its two bits.

unpack(length=None, **kwargs)[source]

Unpack (parse) packet data, with the header schema its first octet selects.

A unified-header record (RFC 9147 Section 4.1) is parsed by DTLSCiphertext, and a tls12_cid one (RFC 9146 Section 4) by DTLSCIDCiphertext, rather than the legacy header; every other record uses the inherited behaviour. Either schema reads a connection ID with the length DTLSContext finds.

Parameters:
  • length (int | None) – Length of packet data.

  • **kwargs (Any) – Arbitrary keyword arguments.

Return type:

DTLS | DTLSCiphertext

Returns:

Parsed packet data.

Raises:

ProtocolError – If a record’s connection ID length is not known.

classmethod read_handshake(message)[source]

Read one handshake message from its own octets, outside any record.

A message’s length is 24-bit (RFC 6347 Section 4.2.2) but a record’s is 16-bit, so a message reassembled from its fragments may be too long for any record to carry to read().

Parameters:

message (bytes) – The handshake header, fragment header and body.

Return type:

DTLSHandshake

Returns:

The message, decoded as read() decodes one in a record.

Raises:

ProtocolError – If message is not exactly one handshake message, or its body is malformed for its msg_type.

classmethod analyze_handshake(header, body)[source]

Analyse a reassembled handshake message from its header and body.

dtls_handshake() runs this, and so does the Deferred that DTLS stores as a complete message’s packet (#2622).

The message is read by read_handshake() as if it had arrived unfragmented, fragment_offset 0 and fragment_length its length (RFC 6347 Section 4.2.3). A body that does not decode as its msg_type stays raw octets. That refusal is handled here, so it is quiet (#2588): nothing is logged and no exception hook installed.

Parameters:
Return type:

DTLSHandshake

Returns:

The message, unfragmented, its body raw if it does not decode.

__length_hint__()[source]

Return an estimated length for the object.

Return type:

Literal[13]

classmethod __index__()[source]

Numeral registry index of the protocol.

Return type:

int

Returns:

Numeral registry index of the protocol in IANA service names and port numbers: UDP port 5684, the port of UDP.coaps as an int, which UDP.__proto__ dispatches here (The Registry Index).

classmethod _make_data(data)[source]

Create key-value pairs from data for protocol construction.

Parameters:

data (DTLS | DTLSCiphertext | DTLSCIDCiphertext) – protocol data

Return type:

dict[str, Any]

Returns:

Key-value pairs for protocol construction.

class pcapkit.protocols.application.dtls.DTLSContext(*connection_ids, cid_length=None)[source]

Bases: ProtocolContext

Connection IDs for the DTLS records that carry one (RFC 9146, RFC 9147).

Neither a tls12_cid record nor a DTLS 1.3 unified header carries its connection ID’s length, so match() finds it: from the connection IDs registered here, then from cid_length. DTLS registers those a ClientHello or ServerHello negotiates, and those of the records a constructed one carries as its payload. Those a DTLS 1.3 NewConnectionId issues are encrypted, so only the caller can register them (RFC 9147 Section 9).

The IDs belong to the instance: a new one starts with none, one reused across parses or extractions keeps what it has learned, and nothing is kept anywhere else. An extraction given no instance gets its own, in its own registry, the first time a hello negotiates an ID.

Matching by value relies on IDs being self-delineating (RFC 9146 Section 4): with c1c2c3c4 registered, a record whose ID is c1c2c3c400010203 reads as carrying c1c2c3c4. Where one registered ID is a prefix of another, a record carrying the shorter can read as carrying the longer, when the octets after it repeat the longer one’s rest and the record still fits; a unified header must also leave room for its record-number sample, which rules most such readings out.

Parameters:
  • *connection_ids (bytes) – Connection IDs known out of band.

  • cid_length (int | None) – Connection ID length of a record that no registered one opens.

Raises:

ProtocolError – If a connection ID is not 1 to 255 octets, or cid_length is not an int from 1 to 255.

property connection_ids: tuple[bytes, ...]

Registered connection IDs, in the order registered.

property cid_length: int | None

Connection ID length of a record that no registered one opens.

classmethod protocol()[source]

Index ID of the protocol this context applies to.

Return type:

tuple[Literal['DTLS']]

register(cid)[source]

Add a connection ID; one already registered is left as it is.

Parameters:

cid (bytes) – Connection ID to add.

Raises:

ProtocolError – If cid is not 1 to 255 octets.

match(octets, *, sequence_length=0, length_present=True, min_content_length=0)[source]

Find the length of the connection ID that octets opens with.

Each registered connection ID that octets begins with is a candidate, longest first, and cid_length comes last. The first whose record ends inside octets and carries at least min_content_length octets of content is the answer. After the ID come sequence_length octets, then a 16-bit length and that many octets, or, without the length, the rest of octets. Failing that it is the first candidate, so that reading the record reports what is wrong with it.

Parameters:
  • octets (bytes) – A record from its connection ID on.

  • sequence_length (int) – Octets between the ID and the length: none in a tls12_cid record (RFC 9146 Section 4), and a unified header’s sequence number (RFC 9147 Section 4).

  • length_present (bool) – Whether the record carries its length, as a unified header’s L bit says; a tls12_cid record always does.

  • min_content_length (int) – Fewest content octets a record may carry: a unified header’s record-number sample (RFC 9147 Section 4.2.3). A tls12_cid record’s minimum depends on its cipher suite, so none is assumed for it.

Return type:

int | None

Returns:

The connection ID’s length, or None with no candidate.

Header Schema

class pcapkit.protocols.schema.application.dtls.DTLS(content_type=NO_VALUE, version=NO_VALUE, epoch=NO_VALUE, sequence_number=NO_VALUE, length=NO_VALUE, fragment=NO_VALUE, payload=NO_VALUE, *, __packet__=None, **kwargs)[source]

Bases: Schema

Header schema for DTLS record layer packet.

Same ContentType, legacy-version and length fields as TLS’s record header, with the explicit epoch/sequence_number pair DTLS inserts between the version and the length (RFC 9147 Section 4) – the divergence a reliable TLS stream never needs, since DTLS runs over an unreliable, unordered datagram transport.

content_type: ContentType = <EnumField content_type>

Content type.

version: int = <UInt16Field version>

Legacy record version (major << 8 | minor).

epoch: int = <UInt16Field epoch>

Epoch – incremented on every change of cipher state.

sequence_number: int = <NumberField sequence_number>

Sequence number within epoch (48-bit).

length: int = <UInt16Field length>

Length of fragment.

fragment: bytes = <BytesField fragment>

Record fragment, undecoded.

payload: bytes = <PayloadField payload>

Octets after the record, e.g. a further record coalesced in the same buffer.

class pcapkit.protocols.schema.application.dtls.DTLSCiphertext(flags=NO_VALUE, cid=NO_VALUE, sequence_number=NO_VALUE, length=NO_VALUE, fragment=NO_VALUE, payload=NO_VALUE, *, __packet__=None, **kwargs)[source]

Bases: Schema

Header schema for a DTLS 1.3 ciphertext record, which has a unified header.

The first octet is 001CSLEE (RFC 9147 Section 4.1): C names a connection ID, S an 8- or 16-bit sequence number, L a length field, and EE the low two bits of the epoch. The record has no content type or version octet, since both are inside the ciphertext, so DTLS’s legacy header cannot describe it.

The record does not carry its connection ID’s length (RFC 9147 Section 4), so the parser gives it as the __cid_length__ key of the packet context; packing takes it from cid.

flags: dict[str, int] = <BitField flags>

Unified-header first octet – fixed prefix, C, S, L and EE.

cid: bytes | None = <ConditionalField cid>

Connection ID; absent when C is clear.

sequence_number: int = <SwitchField sequence_number>

Sequence number, 8 or 16 bits as S says (header-protected on the wire).

length: int | None = <ConditionalField length>

Length of fragment; absent when L is clear.

fragment: bytes = <SwitchField fragment>

Ciphertext, undecoded. Without a length it runs to the end of the buffer.

payload: bytes = <PayloadField payload>

Octets after the record, e.g. a further record coalesced in the same buffer. Empty when fragment runs to the end of the buffer.

pre_pack(packet)[source]

Give packing the length of cid, which the record does not carry.

Parameters:

packet (dict[str, Any]) – packet data

class pcapkit.protocols.schema.application.dtls.DTLSCIDCiphertext(content_type=NO_VALUE, version=NO_VALUE, epoch=NO_VALUE, sequence_number=NO_VALUE, cid=NO_VALUE, length=NO_VALUE, fragment=NO_VALUE, payload=NO_VALUE, *, __packet__=None, **kwargs)[source]

Bases: Schema

Header schema for a DTLS 1.2 record with a connection ID (RFC 9146 Section 4).

DTLS’s legacy header with cid between the sequence number and the length. The record does not carry the CID’s length, so the parser gives it as the __cid_length__ key of the packet context; packing takes it from cid.

content_type: ContentType = <EnumField content_type>

Outer content type, always tls12_cid.

version: int = <UInt16Field version>

Legacy record version (major << 8 | minor).

epoch: int = <UInt16Field epoch>

Epoch – incremented on every change of cipher state.

sequence_number: int = <NumberField sequence_number>

Sequence number within epoch (48-bit).

cid: bytes = <BytesField cid>

Connection ID.

length: int = <UInt16Field length>

Length of fragment.

fragment: bytes = <BytesField fragment>

Encrypted record content, undecoded.

payload: bytes = <PayloadField payload>

Octets after the record, e.g. a further record coalesced in the same buffer.

pre_pack(packet)[source]

Give packing the length of cid, which the record does not carry.

Parameters:

packet (dict[str, Any]) – packet data

Data Models

class pcapkit.protocols.data.application.dtls.DTLS(content_type, version, epoch, sequence_number, length, content)[source]

Bases: Protocol

Data model for DTLS protocol.

content_type: ContentType

Content type.

version: int

Legacy record version (major << 8 | minor).

epoch: int

Epoch – incremented on every change of cipher state.

sequence_number: int

Sequence number within epoch (48-bit).

length: int

Length of the record fragment.

content: Any

Decoded record content. For handshake, alert, and change_cipher_spec, this is a tuple of DTLSHandshake, an Alert, or a ChangeCipherSpec respectively only when the record is cleartext, i.e. epoch == 0 (as decided by SSL._tls_content_is_cleartext) – an encrypted record of any of those three types, which is any record at a later epoch, stays raw bytes instead, same as every other content type.

class pcapkit.protocols.data.application.dtls.DTLSCiphertext(epoch, cid, sequence_number, sequence_long, length, content)[source]

Bases: Protocol

Data model for a DTLS 1.3 ciphertext record (RFC 9147 Section 4).

The unified header carries neither a content type nor a version, and only the low two bits of the epoch, so those fields are absent here: the content type is inside content.

epoch: int

Low two bits of the epoch; the full 16-bit epoch is not on the wire.

cid: bytes | None

Connection ID, as long as the handshake or the caller says, or None when the header carries none.

sequence_number: int

Sequence number as carried, 8 or 16 bits per sequence_long. It is header-protected, so it is the value on the wire, not the record’s.

sequence_long: bool

Whether sequence_number is 16 bits rather than 8.

length: int | None

Length of content, or None when the record runs to the end of the datagram.

content: bytes

Encrypted record content, undecoded.

class pcapkit.protocols.data.application.dtls.DTLSCIDCiphertext(content_type, version, epoch, sequence_number, cid, length, content)[source]

Bases: Protocol

Data model for a DTLS 1.2 record with a connection ID (RFC 9146 Section 4).

The record is always encrypted, so its real content type is inside content and content_type is tls12_cid.

content_type: ContentType

Outer content type, always tls12_cid.

version: int

Legacy record version (major << 8 | minor), DTLS 1.2’s.

epoch: int

Epoch – incremented on every change of cipher state.

sequence_number: int

Sequence number within epoch (48-bit).

cid: bytes

Connection ID, as long as the handshake or the caller says.

length: int

Length of content.

content: bytes

Encrypted record content, undecoded.

class pcapkit.protocols.data.application.dtls.DTLSHandshake(msg_type, length, body, message_seq, fragment_offset, fragment_length, is_fragmented)[source]

Bases: Handshake

Data model for a DTLS handshake message.

DTLS adds a fragment header – message_seq, fragment_offset and fragment_length – on top of the TLS handshake header shared with Handshake (RFC 6347 Section 4.2.2), so that a handshake message that does not fit in one datagram can be split across several.

body decodes to a ClientHello, ServerHello or EncryptedExtensions when is_fragmented is False. A fragmented message’s body is the fragment’s raw octets; dtls_handshake() decodes the message once DTLS has reassembled it (#2440).

msg_type: HandshakeType

Handshake message type.

length: int

Total message length (24-bit), fragment headers and bodies excluded.

body: ClientHello | ServerHello | EncryptedExtensions | bytes

Message body – see the class docstring for when this decodes.

message_seq: int

Message sequence number.

fragment_offset: int

Fragment offset (24-bit) into the reassembled message.

fragment_length: int

Length (24-bit) of this fragment.

is_fragmented: bool

Whether this fragment is a strict subset of the message, i.e. fragment_offset != 0 or fragment_length != length.

See SSL - Secure Sockets Layer for ClientHello, ServerHello, EncryptedExtensions, Extension and its decoded bodies, Alert and ChangeCipherSpec, shared verbatim with TLS.

Footnotes