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 |
|
|---|---|
|
|
|
|
|
|
any other |
|
unified header (first octet |
|
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
ClientHelloorServerHelloconnection_idextension 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_lengththe caller givesDTLSContext.
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
Rawtogether 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'orlayer='Application') keeps the rest of the datagram raw.Each record is a layer, so
FRAME_LAYER_LIMITbounds 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]¶
-
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 withpcapkit.foundation.registry.protocols.transport.register_udp().- 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_cidone; a unified header’s length follows from its first octet and its connection ID.
- read(length=None, **kwargs)[source]¶
Read Datagram Transport Layer Security (DTLS).
- Parameters:
- Return type:
- 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:
versionis written as is.- Parameters:
content_type (
ContentType|int) – Record content type.version (
int) – Legacy record version (major << 8 | minor), e.g.0xFEFDfor 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 withinepoch(48-bit); for a unified-header record, 8 or 16 bits assequence_longsays.content (
Any) – Record content: atupleofDTLSHandshakeforhandshake, anAlert, aChangeCipherSpec, or rawbytes, asread()would decode it. For a unified-header ortls12_cidrecord, the ciphertext asbytes.data (
bytes|None) – Pre-encoded record fragment. When given, it is used verbatim in place ofcontentandlengthis 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 aDTLSpayload 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,versionanddatado 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 atls12_cidrecord (RFC 9146 Section 4), which needs one, or of a unified-header record, which sets itsCbit (RFC 9147 Section 4); 1 to 255 octets. No other record takes one.**kwargs (
Any) – Arbitrary keyword arguments.
- Return type:
- Returns:
Constructed packet data.
- Raises:
ProtocolError – If
cidis invalid, missing for atls12_cidrecord, or given for a legacy-format one of another type; if a legacy-format record’scontent_typeis a name, abool, or a member or code the 8-bit content type field cannot carry; or if an extension ofcontentgiven 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
epochdoes 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 atls12_cidone (RFC 9146 Section 4) byDTLSCIDCiphertext, rather than the legacy header; every other record uses the inherited behaviour. Either schema reads a connection ID with the lengthDTLSContextfinds.- Parameters:
- Return type:
- 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
lengthis 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 toread().- Parameters:
message (
bytes) – The handshake header, fragment header and body.- Return type:
- Returns:
The message, decoded as
read()decodes one in a record.- Raises:
ProtocolError – If
messageis not exactly one handshake message, or its body is malformed for itsmsg_type.
- classmethod analyze_handshake(header, body)[source]¶
Analyse a reassembled handshake message from its header and body.
dtls_handshake()runs this, and so does theDeferredthatDTLSstores as a complete message’spacket(#2622).The message is read by
read_handshake()as if it had arrived unfragmented,fragment_offset0 andfragment_lengthitslength(RFC 6347 Section 4.2.3). A body that does not decode as itsmsg_typestays raw octets. That refusal is handled here, so it is quiet (#2588): nothing is logged and no exception hook installed.- Parameters:
header (
tuple[HandshakeType,int,int]) – The message’s(msg_type, length, message_seq).body (
bytes) – The reassembled body.
- Return type:
- Returns:
The message, unfragmented, its body raw if it does not decode.
- classmethod __index__()[source]¶
Numeral registry index of the protocol.
- Return type:
- Returns:
Numeral registry index of the protocol in IANA service names and port numbers: UDP port 5684, the port of
UDP.coapsas anint, whichUDP.__proto__dispatches here (The Registry Index).
- classmethod _make_data(data)[source]¶
Create key-value pairs from
datafor protocol construction.- Parameters:
data (
DTLS|DTLSCiphertext|DTLSCIDCiphertext) – protocol data- Return type:
- Returns:
Key-value pairs for protocol construction.
- class pcapkit.protocols.application.dtls.DTLSContext(*connection_ids, cid_length=None)[source]¶
Bases:
ProtocolContextConnection IDs for the
DTLSrecords that carry one (RFC 9146, RFC 9147).Neither a
tls12_cidrecord nor a DTLS 1.3 unified header carries its connection ID’s length, somatch()finds it: from the connection IDs registered here, then fromcid_length.DTLSregisters those aClientHelloorServerHellonegotiates, and those of the records a constructed one carries as its payload. Those a DTLS 1.3NewConnectionIdissues 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
c1c2c3c4registered, a record whose ID isc1c2c3c400010203reads as carryingc1c2c3c4. 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:
- Raises:
ProtocolError – If a connection ID is not 1 to 255 octets, or
cid_lengthis not anintfrom 1 to 255.
- 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
cidis 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
octetsopens with.Each registered connection ID that
octetsbegins with is a candidate, longest first, andcid_lengthcomes last. The first whose record ends insideoctetsand carries at leastmin_content_lengthoctets of content is the answer. After the ID comesequence_lengthoctets, then a 16-bit length and that many octets, or, without the length, the rest ofoctets. 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 atls12_cidrecord (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’sLbit says; atls12_cidrecord 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). Atls12_cidrecord’s minimum depends on its cipher suite, so none is assumed for it.
- Return type:
- Returns:
The connection ID’s length, or
Nonewith 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:
SchemaHeader schema for DTLS record layer packet.
Same
ContentType, legacy-version and length fields asTLS’s record header, with the explicitepoch/sequence_numberpair 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.
- 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:
SchemaHeader schema for a DTLS 1.3 ciphertext record, which has a unified header.
The first octet is
001CSLEE(RFC 9147 Section 4.1):Cnames a connection ID,San 8- or 16-bit sequence number,La length field, andEEthe low two bits of the epoch. The record has no content type or version octet, since both are inside the ciphertext, soDTLS’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 fromcid.- flags: dict[str, int] = <BitField flags>¶
Unified-header first octet – fixed prefix,
C,S,LandEE.
- sequence_number: int = <SwitchField sequence_number>¶
Sequence number, 8 or 16 bits as
Ssays (header-protected on the wire).
- fragment: bytes = <SwitchField fragment>¶
Ciphertext, undecoded. Without a length it runs to the end of the buffer.
- 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:
SchemaHeader schema for a DTLS 1.2 record with a connection ID (RFC 9146 Section 4).
DTLS’s legacy header withcidbetween 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 fromcid.- content_type: ContentType = <EnumField content_type>¶
Outer content type, always
tls12_cid.
Data Models¶
- class pcapkit.protocols.data.application.dtls.DTLS(content_type, version, epoch, sequence_number, length, content)[source]¶
Bases:
ProtocolData model for DTLS protocol.
- content_type: ContentType¶
Content type.
- content: Any¶
Decoded record content. For
handshake,alert, andchange_cipher_spec, this is a tuple ofDTLSHandshake, anAlert, or aChangeCipherSpecrespectively only when the record is cleartext, i.e.epoch == 0(as decided bySSL._tls_content_is_cleartext) – an encrypted record of any of those three types, which is any record at a later epoch, stays rawbytesinstead, same as every other content type.
- class pcapkit.protocols.data.application.dtls.DTLSCiphertext(epoch, cid, sequence_number, sequence_long, length, content)[source]¶
Bases:
ProtocolData 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.- cid: bytes | None¶
Connection ID, as long as the handshake or the caller says, or
Nonewhen 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_numberis 16 bits rather than 8.
- class pcapkit.protocols.data.application.dtls.DTLSCIDCiphertext(content_type, version, epoch, sequence_number, cid, length, content)[source]¶
Bases:
ProtocolData 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
contentandcontent_typeistls12_cid.- content_type: ContentType¶
Outer content type, always
tls12_cid.
- class pcapkit.protocols.data.application.dtls.DTLSHandshake(msg_type, length, body, message_seq, fragment_offset, fragment_length, is_fragmented)[source]¶
Bases:
HandshakeData model for a DTLS handshake message.
DTLS adds a fragment header –
message_seq,fragment_offsetandfragment_length– on top of the TLS handshake header shared withHandshake(RFC 6347 Section 4.2.2), so that a handshake message that does not fit in one datagram can be split across several.bodydecodes to aClientHello,ServerHelloorEncryptedExtensionswhenis_fragmentedisFalse. A fragmented message’sbodyis the fragment’s raw octets;dtls_handshake()decodes the message onceDTLShas reassembled it (#2440).- msg_type: HandshakeType¶
Handshake message type.
- body: ClientHello | ServerHello | EncryptedExtensions | bytes¶
Message body – see the class docstring for when this decodes.
See SSL - Secure Sockets Layer for ClientHello, ServerHello, EncryptedExtensions,
Extension and its decoded bodies, Alert and ChangeCipherSpec,
shared verbatim with TLS.
Footnotes