Files
openthread/tests/scripts/thread-cert/dtls.py
T
Abtin Keshavarzian 71e00eb20f [test] add missing tests in the Makefile.am & update file access mode (#6825)
This commits updates `thread-cert/Makefile.am` adding some existing
tests that were not included in the Makefile when the tests were
added. It also sorts the entries to be in alphabetical order.

This commit also updates the access control mode for all script files
to be executable by all (`chmod 755` or "-rwxr-xr-x").
2021-07-19 14:35:50 -07:00

646 lines
20 KiB
Python
Executable File

#!/usr/bin/env python3
#
# Copyright (c) 2019, The OpenThread Authors.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
from enum import IntEnum
from functools import reduce
import io
import struct
from ipv6 import BuildableFromBytes
from ipv6 import ConvertibleToBytes
class HandshakeType(IntEnum):
HELLO_REQUEST = 0
CLIENT_HELLO = 1
SERVER_HELLO = 2
HELLO_VERIFY_REQUEST = 3
CERTIFICATE = 11
SERVER_KEY_EXCHANGE = 12
CERTIFICATE_REQUEST = 13
SERVER_HELLO_DONE = 14
CERTIFICATE_VERIFY = 15
CLIENT_KEY_EXCHANGE = 16
FINISHED = 20
class ContentType(IntEnum):
CHANGE_CIPHER_SPEC = 20
ALERT = 21
HANDSHAKE = 22
APPLICATION_DATA = 23
class AlertLevel(IntEnum):
WARNING = 1
FATAL = 2
class AlertDescription(IntEnum):
CLOSE_NOTIFY = 0
UNEXPECTED_MESSAGE = 10
BAD_RECORD_MAC = 20
DECRYPTION_FAILED_RESERVED = 21
RECORD_OVERFLOW = 22
DECOMPRESSION_FAILURE = 30
HANDSHAKE_FAILURE = 40
NO_CERTIFICATE_RESERVED = 41
BAD_CERTIFICATE = 42
UNSUPPORTED_CERTIFICATE = 43
CERTIFICATE_REVOKED = 44
CERTIFICATE_EXPIRED = 45
CERTIFICATE_UNKNOWN = 46
ILLEGAL_PARAMETER = 47
UNKNOWN_CA = 48
ACCESS_DENIED = 49
DECODE_ERROR = 50
DECRYPT_ERROR = 51
EXPORT_RESTRICTION_RESERVED = 60
PROTOCOL_VERSION = 70
INSUFFICIENT_SECURITY = 71
INTERNAL_ERROR = 80
USER_CANCELED = 90
NO_RENEGOTIATION = 100
UNSUPPORTED_EXTENSION = 110
class Record(ConvertibleToBytes, BuildableFromBytes):
def __init__(self, content_type, version, epoch, sequence_number, length, fragment):
self.content_type = content_type
self.version = version
self.epoch = epoch
self.sequence_number = sequence_number
self.length = length
self.fragment = fragment
def to_bytes(self):
return (struct.pack(">B", self.content_type) + self.version.to_bytes() + struct.pack(">H", self.epoch) +
self.sequence_number.to_bytes(6, byteorder='big') + struct.pack(">H", self.length) + self.fragment)
@classmethod
def from_bytes(cls, data):
content_type = ContentType(struct.unpack(">B", data.read(1))[0])
version = ProtocolVersion.from_bytes(data)
epoch = struct.unpack(">H", data.read(2))[0]
sequence_number = struct.unpack(">Q", b'\x00\x00' + data.read(6))[0]
length = struct.unpack(">H", data.read(2))[0]
fragment = bytes(data.read(length))
return cls(content_type, version, epoch, sequence_number, length, fragment)
def __repr__(self):
return "Record(content_type={}, version={}, epoch={}, sequence_number={}, length={})".format(
str(self.content_type),
self.version,
self.epoch,
self.sequence_number,
self.length,
)
class Message(ConvertibleToBytes, BuildableFromBytes):
def __init__(self, content_type):
self.content_type = content_type
def to_bytes(self):
raise NotImplementedError
@classmethod
def from_bytes(cls, data):
raise NotImplementedError
class HandshakeMessage(Message):
def __init__(
self,
handshake_type,
length,
message_seq,
fragment_offset,
fragment_length,
body,
):
super(HandshakeMessage, self).__init__(ContentType.HANDSHAKE)
self.handshake_type = handshake_type
self.length = length
self.message_seq = message_seq
self.fragment_offset = fragment_offset
self.fragment_length = fragment_length
self.body = body
def to_bytes(self):
return (struct.pack(">B", self.handshake_type) + struct.pack(">I", self.length)[1:] +
struct.pack(">H", self.message_seq) + struct.pack(">I", self.fragment_offset)[1:] +
struct.pack(">I", self.fragment_length)[1:] + self.body.to_bytes())
@classmethod
def from_bytes(cls, data):
handshake_type = HandshakeType(struct.unpack(">B", data.read(1))[0])
length = struct.unpack(">I", b'\x00' + data.read(3))[0]
message_seq = struct.unpack(">H", data.read(2))[0]
fragment_offset = struct.unpack(">I", b'\x00' + bytes(data.read(3)))[0]
fragment_length = struct.unpack(">I", b'\x00' + bytes(data.read(3)))[0]
end_position = data.tell() + fragment_length
# TODO(wgtdkp): handle fragmentation
message_class, body = handshake_map[handshake_type], None
if message_class:
body = message_class.from_bytes(data)
else:
print("{} messages are not handled".format(str(handshake_type)))
body = bytes(data.read(fragment_length))
assert data.tell() == end_position
return cls(
handshake_type,
length,
message_seq,
fragment_offset,
fragment_length,
body,
)
def __repr__(self):
return "Handshake(type={}, length={})".format(str(self.handshake_type), self.length)
class ProtocolVersion(ConvertibleToBytes, BuildableFromBytes):
def __init__(self, major, minor):
self.major = major
self.minor = minor
def __eq__(self, other):
return (isinstance(self, type(other)) and self.major == other.major and self.minor == other.minor)
def to_bytes(self):
return struct.pack(">BB", self.major, self.minor)
@classmethod
def from_bytes(cls, data):
major, minor = struct.unpack(">BB", data.read(2))
return cls(major, minor)
def __repr__(self):
return "ProtocolVersion(major={}, minor={})".format(self.major, self.minor)
class Random(ConvertibleToBytes, BuildableFromBytes):
random_bytes_length = 28
def __init__(self, gmt_unix_time, random_bytes):
self.gmt_unix_time = gmt_unix_time
self.random_bytes = random_bytes
assert len(self.random_bytes) == Random.random_bytes_length
def __eq__(self, other):
return (isinstance(self, type(other)) and self.gmt_unix_time == other.gmt_unix_time and
self.random_bytes == other.random_bytes)
def to_bytes(self):
return struct.pack(">I", self.gmt_unix_time) + (self.random_bytes)
@classmethod
def from_bytes(cls, data):
gmt_unix_time = struct.unpack(">I", data.read(4))[0]
random_bytes = bytes(data.read(cls.random_bytes_length))
return cls(gmt_unix_time, random_bytes)
class VariableVector(ConvertibleToBytes):
def __init__(self, subrange, ele_cls, elements):
self.subrange = subrange
self.ele_cls = ele_cls
self.elements = elements
assert self.subrange[0] <= len(self.elements) <= self.subrange[1]
def length(self):
return len(self.elements)
def __eq__(self, other):
return (isinstance(self, type(other)) and self.subrange == other.subrange and self.ele_cls == other.ele_cls and
self.elements == other.elements)
def to_bytes(self):
data = reduce(lambda ele, acc: acc + ele.to_bytes(), self.elements)
return VariableVector._encode_length(len(data), self.subrange) + data
@classmethod
def from_bytes(cls, ele_cls, subrange, data):
length = cls._decode_length(subrange, data)
end_position = data.tell() + length
elements = []
while data.tell() < end_position:
elements.append(ele_cls.from_bytes(data))
return cls(subrange, ele_cls, elements)
@classmethod
def _decode_length(cls, subrange, data):
length_in_byte = cls._calc_length_in_byte(subrange[1])
return reduce(
lambda acc, byte: (acc << 8) | byte,
bytearray(data.read(length_in_byte)),
0,
)
@classmethod
def _encode_length(cls, length, subrange):
length_in_byte = cls._calc_length_in_byte(subrange[1])
ret = bytearray([])
while length_in_byte > 0:
ret += bytes(length_in_byte & 0xff)
length_in_byte = length_in_byte >> 8
return ret
@classmethod
def _calc_length_in_byte(cls, ceiling):
return (ceiling.bit_length() + 7) // 8
class Opaque(ConvertibleToBytes, BuildableFromBytes):
def __init__(self, byte):
self.byte = byte
def __eq__(self, other):
return isinstance(self, type(other)) and self.byte == other.byte
def to_bytes(self):
return struct.pack(">B", self.byte)
@classmethod
def from_bytes(cls, data):
return cls(struct.unpack(">B", data.read(1))[0])
class CipherSuite(ConvertibleToBytes, BuildableFromBytes):
def __init__(self, cipher):
self.cipher = cipher
def __eq__(self, other):
return isinstance(self, type(other)) and self.cipher == other.cipher
def to_bytes(self):
return struct.pack(">BB", self.cipher[0], self.cipher[1])
@classmethod
def from_bytes(cls, data):
return cls(struct.unpack(">BB", data.read(2)))
def __repr__(self):
return "CipherSuite({}, {})".format(self.cipher[0], self.cipher[1])
class CompressionMethod(ConvertibleToBytes, BuildableFromBytes):
NULL = 0
def __init__(self):
pass
def __eq__(self, other):
return isinstance(self, type(other))
def to_bytes(self):
return struct.pack(">B", CompressionMethod.NULL)
@classmethod
def from_bytes(cls, data):
method = struct.unpack(">B", data.read(1))[0]
assert method == cls.NULL
return cls()
class Extension(ConvertibleToBytes, BuildableFromBytes):
def __init__(self, extension_type, extension_data):
self.extension_type = extension_type
self.extension_data = extension_data
def __eq__(self, other):
return (isinstance(self, type(other)) and self.extension_type == other.extension_type and
self.extension_data == other.extension_data)
def to_bytes(self):
return (struct.pack(">H", self.extension_type) + self.extension_data.to_bytes())
@classmethod
def from_bytes(cls, data):
extension_type = struct.unpack(">H", data.read(2))[0]
extension_data = VariableVector.from_bytes(Opaque, (0, 2**16 - 1), data)
return cls(extension_type, extension_data)
class ClientHello(HandshakeMessage):
def __init__(
self,
client_version,
random,
session_id,
cookie,
cipher_suites,
compression_methods,
extensions,
):
self.client_version = client_version
self.random = random
self.session_id = session_id
self.cookie = cookie
self.cipher_suites = cipher_suites
self.compression_methods = compression_methods
self.extensions = extensions
def to_bytes(self):
return (self.client_version.to_bytes() + self.random.to_bytes() + self.session_id.to_bytes() +
self.cookie.to_bytes() + self.cipher_suites.to_bytes() + self.compression_methods.to_bytes() +
self.extensions.to_bytes())
@classmethod
def from_bytes(cls, data):
client_version = ProtocolVersion.from_bytes(data)
random = Random.from_bytes(data)
session_id = VariableVector.from_bytes(Opaque, (0, 32), data)
cookie = VariableVector.from_bytes(Opaque, (0, 2**8 - 1), data)
cipher_suites = VariableVector.from_bytes(CipherSuite, (2, 2**16 - 1), data)
compression_methods = VariableVector.from_bytes(CompressionMethod, (1, 2**8 - 1), data)
extensions = None
if data.tell() < len(data.getvalue()):
extensions = VariableVector.from_bytes(Extension, (0, 2**16 - 1), data)
return cls(
client_version,
random,
session_id,
cookie,
cipher_suites,
compression_methods,
extensions,
)
class HelloVerifyRequest(HandshakeMessage):
def __init__(self, server_version, cookie):
self.server_version = server_version
self.cookie = cookie
def to_bytes(self):
return self.server_version.to_bytes() + self.cookie.to_bytes()
@classmethod
def from_bytes(cls, data):
server_version = ProtocolVersion.from_bytes(data)
cookie = VariableVector.from_bytes(Opaque, (0, 2**8 - 1), data)
return cls(server_version, cookie)
class ServerHello(HandshakeMessage):
def __init__(
self,
server_version,
random,
session_id,
cipher_suite,
compression_method,
extensions,
):
self.server_version = server_version
self.random = random
self.session_id = session_id
self.cipher_suite = cipher_suite
self.compression_method = compression_method
self.extensions = extensions
def to_bytes(self):
return (self.server_version.to_bytes() + self.random.to_bytes() + self.session_id.to_bytes() +
self.cipher_suite.to_bytes() + self.compression_method.to_bytes() + self.extensions.to_bytes())
@classmethod
def from_bytes(cls, data):
server_version = ProtocolVersion.from_bytes(data)
random = Random.from_bytes(data)
session_id = VariableVector.from_bytes(Opaque, (0, 32), data)
cipher_suite = CipherSuite.from_bytes(data)
compression_method = CompressionMethod.from_bytes(data)
extensions = None
if data.tell() < len(data.getvalue()):
extensions = VariableVector.from_bytes(Extension, (0, 2**16 - 1), data)
return cls(
server_version,
random,
session_id,
cipher_suite,
compression_method,
extensions,
)
class ServerHelloDone(HandshakeMessage):
def __init__(self):
pass
def to_bytes(self):
return bytearray([])
@classmethod
def from_bytes(cls, data):
return cls()
class HelloRequest(HandshakeMessage):
def __init__(self):
raise NotImplementedError
class Certificate(HandshakeMessage):
def __init__(self):
raise NotImplementedError
class ServerKeyExchange(HandshakeMessage):
def __init__(self):
raise NotImplementedError
class CertificateRequest(HandshakeMessage):
def __init__(self):
raise NotImplementedError
class CertificateVerify(HandshakeMessage):
def __init__(self):
raise NotImplementedError
class ClientKeyExchange(HandshakeMessage):
def __init__(self):
raise NotImplementedError
class Finished(HandshakeMessage):
def __init__(self, verify_data):
raise NotImplementedError
class AlertMessage(Message):
def __init__(self, level, description):
super(AlertMessage, self).__init__(ContentType.ALERT)
self.level = level
self.description = description
def to_bytes(self):
struct.pack(">BB", self.level, self.description)
@classmethod
def from_bytes(cls, data):
level, description = struct.unpack(">BB", data.read(2))
try:
return cls(AlertLevel(level), AlertDescription(description))
except BaseException:
data.read()
# An AlertMessage could be encrypted and we can't parsing it.
return cls(None, None)
def __repr__(self):
return "Alert(level={}, description={})".format(str(self.level), str(self.description))
class ChangeCipherSpecMessage(Message):
def __init__(self):
super(ChangeCipherSpecMessage, self).__init__(ContentType.CHANGE_CIPHER_SPEC)
def to_bytes(self):
return struct.pack(">B", 1)
@classmethod
def from_bytes(cls, data):
assert struct.unpack(">B", data.read(1))[0] == 1
return cls()
def __repr__(self):
return "ChangeCipherSpec(value=1)"
class ApplicationDataMessage(Message):
def __init__(self, raw):
super(ApplicationDataMessage, self).__init__(ContentType.APPLICATION_DATA)
self.raw = raw
self.body = None
def to_bytes(self):
return self.raw
@classmethod
def from_bytes(cls, data):
# It is safe to read until the end of this byte stream, because
# there is single application data message in a record.
length = len(data.getvalue()) - data.tell()
return cls(bytes(data.read(length)))
def __repr__(self):
if self.body:
return "ApplicationData(body={})".format(self.body)
else:
return "ApplicationData(raw_length={})".format(len(self.raw))
handshake_map = {
HandshakeType.HELLO_REQUEST: None, # HelloRequest
HandshakeType.CLIENT_HELLO: ClientHello,
HandshakeType.SERVER_HELLO: ServerHello,
HandshakeType.HELLO_VERIFY_REQUEST: HelloVerifyRequest,
HandshakeType.CERTIFICATE: None, # Certificate
HandshakeType.SERVER_KEY_EXCHANGE: None, # ServerKeyExchange
HandshakeType.CERTIFICATE_REQUEST: None, # CertificateRequest
HandshakeType.SERVER_HELLO_DONE: ServerHelloDone,
HandshakeType.CERTIFICATE_VERIFY: None, # CertificateVerify
HandshakeType.CLIENT_KEY_EXCHANGE: None, # ClientKeyExchange
HandshakeType.FINISHED: None, # Finished
}
content_map = {
ContentType.CHANGE_CIPHER_SPEC: ChangeCipherSpecMessage,
ContentType.ALERT: AlertMessage,
ContentType.HANDSHAKE: HandshakeMessage,
ContentType.APPLICATION_DATA: ApplicationDataMessage,
}
class MessageFactory(object):
last_msg_is_change_cipher_spec = False
def __init__(self):
pass
def parse(self, data, message_info):
messages = []
# Multiple records could be sent in the same UDP datagram
while data.tell() < len(data.getvalue()):
record = Record.from_bytes(data)
if record.version.major != 0xfe or record.version.minor != 0xFD:
raise ValueError("DTLS version error, expect DTLSv1.2")
last_msg_is_change_cipher_spec = type(self).last_msg_is_change_cipher_spec
type(self).last_msg_is_change_cipher_spec = (record.content_type == ContentType.CHANGE_CIPHER_SPEC)
# FINISHED message immediately follows CHANGE_CIPHER_SPEC message
# We skip FINISHED message as it is encrypted
if last_msg_is_change_cipher_spec:
continue
fragment_data = io.BytesIO(record.fragment)
# Multiple handshake messages could be sent in the same record
while fragment_data.tell() < len(fragment_data.getvalue()):
content_class = content_map[record.content_type]
assert content_class
messages.append(content_class.from_bytes(fragment_data))
return messages