Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse client_certificate_url extension and support CertificateURL message #112

Merged
merged 27 commits into from
Dec 6, 2016
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9c4e4f5
Implement CertificateURL constructs
ashfall Nov 23, 2016
d43b754
Add some new alerts to the AlertDescription enum
ashfall Nov 23, 2016
dbcb7db
Define an object representation of a URLAndHash struct
ashfall Nov 23, 2016
6fa7c53
Define CertificateURL.as_bytes()
ashfall Nov 23, 2016
8c732bb
Something about not repeating yourself
ashfall Nov 23, 2016
f1d59a1
A barebones CertificateURL.from_bytes
ashfall Nov 23, 2016
eba6d74
Implement CertificateURL.from_bytes
ashfall Dec 1, 2016
a322860
Raise an exception when the padding byte is not 1
ashfall Dec 1, 2016
ca55815
implement CertificateURL.as_bytes()
ashfall Dec 1, 2016
188d7a3
whitespace
ashfall Dec 1, 2016
c1917f2
Remove duplicate note
ashfall Dec 1, 2016
41c8714
Add a test to parse a ClientHello message with CLIENT_CERTIFICATE_URL…
ashfall Dec 1, 2016
346a63b
Add a test for serializing a ClientHello message with CLIENT_CERTIFIC…
ashfall Dec 1, 2016
3aa50d1
Define a minimal construct for the ClientCertificateURL extension so …
ashfall Dec 1, 2016
4be9ced
pep8
ashfall Dec 1, 2016
3e1758a
Add CertificateURL as a handshake message type and parse Handshake st…
ashfall Dec 1, 2016
17a3280
Define a TLSException that other more specific exceptions inherit from
ashfall Dec 3, 2016
18c869d
Validate padding early, and a failing test
ashfall Dec 3, 2016
f839d07
Define a custom TLSOneOf method that uses TLSExprValidator to raise a…
ashfall Dec 3, 2016
e9b6c84
pep8 + docstrings
ashfall Dec 3, 2016
800ad1f
Delete outdated import
ashfall Dec 3, 2016
fe03620
Tests for TLSExprValidator and TLSOneOf
ashfall Dec 3, 2016
3ede2d6
Use list comprehension to build the url_and_hash_list
ashfall Dec 3, 2016
cd8d518
Tell travis that subconstruct is a thing
ashfall Dec 3, 2016
77e451d
remove inline XXX
ashfall Dec 3, 2016
1cc0cd4
Data is really a Container, asserting to that is better than assertin…
ashfall Dec 6, 2016
dd69365
Update docstring for TLSExprValidator to include why we needed to rei…
ashfall Dec 6, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions tls/_common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class HandshakeType(Enum):
CERTIFICATE_VERIFY = 15
CLIENT_KEY_EXCHANGE = 16
FINISHED = 20
CERTIFICATE_URL = 21
CERTIFICATE_STATUS = 22
Copy link
Member

Choose a reason for hiding this comment

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

Do you think a later PR can make the Handshake construct use EnumSwitch to dispatch to the various handshake types?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes!! I'll play around with that next, blends in well with the #106 branch I was thinking of tackling.



class ContentType(Enum):
Expand Down Expand Up @@ -85,6 +87,10 @@ class AlertDescription(Enum):
USER_CANCELED = 90
NO_RENEGOTIATION = 100
UNSUPPORTED_EXTENSION = 110
CERTIFICATE_UNOBTAINABLE = 111
UNRECOGNIZED_NAME = 112
BAD_CERTIFICATE_STATUS_RESPONSE = 113
BAD_CERTIFICATE_HASH_VALUE = 114


class ExtensionType(Enum):
Expand Down Expand Up @@ -128,3 +134,8 @@ class CompressionMethod(Enum):

class NameType(Enum):
HOST_NAME = 0


class CertChainType(Enum):
INDIVIDUAL_CERTS = 0
PKIPATH = 1
23 changes: 23 additions & 0 deletions tls/_constructs.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@

ServerNameList = TLSPrefixedArray("server_name_list", ServerName)

ClientCertificateURL = Struct(
"client_certificate_url",
# The "extension_data" field of this extension SHALL be empty.
)

SignatureAndHashAlgorithm = Struct(
"algorithms",
EnumClass(UBInt8("hash"), enums.HashAlgorithm),
Expand All @@ -107,6 +112,9 @@
enums.ExtensionType.SIGNATURE_ALGORITHMS: Opaque(
SupportedSignatureAlgorithms
),
enums.ExtensionType.CLIENT_CERTIFICATE_URL: Opaque(
ClientCertificateURL
),
},
default=Pass,
)
Expand Down Expand Up @@ -195,3 +203,18 @@
UBInt8("level"),
UBInt8("description"),
)

URLAndHash = Struct(
"url_and_hash",
SizeWithin(UBInt16("length"),
min_size=1, max_size=2 ** 16 - 1),
Bytes("url", lambda ctx: ctx.length),
UBInt8("padding"),
Bytes("sha1_hash", 20),
)

CertificateURL = Struct(
"CertificateURL",
EnumClass(UBInt8("type"), enums.CertChainType),
TLSPrefixedArray("url_and_hash_list", URLAndHash),
)
7 changes: 7 additions & 0 deletions tls/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ class UnsupportedCipherException(Exception):

class UnsupportedExtensionException(Exception):
pass


class InvalidPaddingException(Exception):
# XXX: Is leaking this info in CertificateURL (i.e. revealing the specific
Copy link
Member

Choose a reason for hiding this comment

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

I think that at this level the code should be as specific as possible. I'd really like tls to have the ability to do better than OpenSSL alert handshake failure.

Upper level code can always suppress these exceptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, thanks! I'll add the TLSException definition, and spin up a new small PR to update UnsupportedExtensionException and UnsupportedCipherException to inherit from it as well.

# byte that is invalid) a good idea? Need to check -- could be a
# vulnerability in disguise.
pass
77 changes: 75 additions & 2 deletions tls/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

from tls._common import enums

from tls.exceptions import InvalidPaddingException

from tls.hello_message import ClientHello, ProtocolVersion, ServerHello


Expand Down Expand Up @@ -204,6 +206,73 @@ def from_bytes(cls, bytes):
)


@attr.s
class URLAndHash(object):
"""
An object representing a URLAndHash struct.
"""
url = attr.ib()
padding = attr.ib()
sha1_hash = attr.ib()

# XXX: The "padding" byte MUST be 0x01. It is
Copy link
Member

Choose a reason for hiding this comment

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

You could use an attrs validator here, as it'd cover both serialization and deserialization.

But since this is a parsing issue, it might be nice to fail early when reading potentially malicious data. You can use construct.OneOf to do that:

>>> construct.OneOf(construct.UBInt8('a'), [1]).parse('\x01')
1
>>> construct.OneOf(construct.UBInt8('a'), [1]).build(1)
'\x01'
>>> construct.OneOf(construct.UBInt8('a'), [1]).parse('\x02')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/mrw/.virtualenvs/tls/local/lib/python2.7/site-packages/construct/core.py", line 188, in parse
    return self.parse_stream(BytesIO(data))
  File "/home/mrw/.virtualenvs/tls/local/lib/python2.7/site-packages/construct/core.py", line 198, in parse_stream
    return self._parse(stream, Container())
  File "/home/mrw/.virtualenvs/tls/local/lib/python2.7/site-packages/construct/core.py", line 288, in _parse
    return self._decode(self.subcon._parse(stream, context), context)
  File "/home/mrw/.virtualenvs/tls/local/lib/python2.7/site-packages/construct/adapters.py", line 412, in _decode
    raise ValidationError("invalid object", obj)
construct.adapters.ValidationError: ('invalid object', 2)
>>> construct.OneOf(construct.UBInt8('a'), [1]).build(2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/mrw/.virtualenvs/tls/local/lib/python2.7/site-packages/construct/core.py", line 212, in build
    self.build_stream(obj, stream)
  File "/home/mrw/.virtualenvs/tls/local/lib/python2.7/site-packages/construct/core.py", line 219, in build_stream
    self._build(obj, stream, Container())
  File "/home/mrw/.virtualenvs/tls/local/lib/python2.7/site-packages/construct/core.py", line 290, in _build
    self.subcon._build(self._encode(obj, context), stream, context)
  File "/home/mrw/.virtualenvs/tls/local/lib/python2.7/site-packages/construct/adapters.py", line 415, in _encode
    return self._decode(obj, context)
  File "/home/mrw/.virtualenvs/tls/local/lib/python2.7/site-packages/construct/adapters.py", line 412, in _decode
    raise ValidationError("invalid object", obj)
construct.adapters.ValidationError: ('invalid object', 2)

Note that failing early on parsing means failing late on serialization. Maybe it's not a bad idea to have an attrs validator and construct.OneOf?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, this is useful! No obvious thoughts on failing early/late, let me play around with this and see what the tests look like. We can do multiple iterations on this until everyone is happy! Updates coming soon!

# present to make the structure backwards
# compatible.
# Should the validation for padding go here?


@attr.s
class CertificateURL(object):
"""
An object representing a CertificateURL struct.
"""
type = attr.ib()
url_and_hash_list = attr.ib()

def as_bytes(self):
for url_and_hash in self.url_and_hash_list:
if url_and_hash.padding != 1:
raise InvalidPaddingException
return _constructs.CertificateURL.build(Container(
type=self.type,
url_and_hash_list=ListContainer(
Container(
length=len(url_and_hash.url),
url=url_and_hash.url,
padding=url_and_hash.padding,
sha1_hash=url_and_hash.sha1_hash,
)
for url_and_hash in self.url_and_hash_list
)
))

@classmethod
def from_bytes(cls, bytes):
"""
Parse a ``CertificateURL`` struct.

:param bytes: the bytes representing the input.
:return: CertificateURL object.
"""
construct = _constructs.CertificateURL.parse(bytes)
url_and_hash_list = []
for url_and_hash in construct.url_and_hash_list:
# TODO: Figure how attrs does validation and move this assert to
# URLAndHash
if url_and_hash.padding != 1:
raise InvalidPaddingException
url_and_hash_list.append(URLAndHash(
url=url_and_hash.url,
padding=url_and_hash.padding,
sha1_hash=url_and_hash.sha1_hash,
))

return cls(
type=construct.type,
url_and_hash_list=url_and_hash_list,
)


@attr.s
class Finished(object):
verify_data = attr.ib()
Expand All @@ -230,7 +299,8 @@ def as_bytes(self):
enums.HandshakeType.CERTIFICATE_REQUEST,
enums.HandshakeType.HELLO_REQUEST,
enums.HandshakeType.SERVER_HELLO_DONE,
enums.HandshakeType.FINISHED
enums.HandshakeType.FINISHED,
enums.HandshakeType.CERTIFICATE_URL,
]:
_body_as_bytes = self.body.as_bytes()
else:
Expand Down Expand Up @@ -271,6 +341,8 @@ def _get_handshake_message(msg_type, body):
CertificateRequest.from_bytes,
# 15: parse_certificate_verify,
# 16: parse_client_key_exchange,
enums.HandshakeType.CERTIFICATE_URL: CertificateURL.from_bytes,
# 22: parse certificate_status,
}

try:
Expand All @@ -282,7 +354,8 @@ def _get_handshake_message(msg_type, body):
return Finished(verify_data=body)
elif msg_type in [enums.HandshakeType.SERVER_KEY_EXCHANGE,
enums.HandshakeType.CERTIFICATE_VERIFY,
enums.HandshakeType.CLIENT_KEY_EXCHANGE]:
enums.HandshakeType.CLIENT_KEY_EXCHANGE,
enums.HandshakeType.CERTIFICATE_STATUS]:
raise NotImplementedError
else:
return _handshake_message_parser[msg_type](body)
Expand Down
35 changes: 35 additions & 0 deletions tls/test/test_hello_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ class TestClientHello(object):
b'\x00\x12'
) + server_name_extension_data

client_certificate_url_extension = (
b'\x00\x02' # Extension Type: Server Certificate Type
b'\x00\x00' # Length
b'' # Data
)

client_hello_packet_with_client_certificate_url_extension = (
common_client_hello_data +
b'\x00\x04'
) + client_certificate_url_extension

def test_resumption_no_extensions(self):
"""
:func:`parse_client_hello` returns an instance of
Expand Down Expand Up @@ -219,6 +230,30 @@ def test_hello_from_bytes_with_unsupported_extension(self):
client_hello_packet
)

def test_parse_client_certificate_url_extension(self):
"""
:py:func:`tls.hello_message.ClientHello` parses a packet with
CLIENT_CERTIFICATE_URL extension.
"""
record = ClientHello.from_bytes(
self.client_hello_packet_with_client_certificate_url_extension
)
assert len(record.extensions) == 1
assert (record.extensions[0].type ==
enums.ExtensionType.CLIENT_CERTIFICATE_URL)
assert record.extensions[0].data == {}
Copy link
Member

Choose a reason for hiding this comment

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

I'd prefer this be

assert record.extensions[0].data == Container()

Since data is actually a Container and I prefer to forget that Container subclasses dict.


def test_as_bytes_client_certificate_url_extension(self):
"""
:py:func:`tls.hello_message.ClientHello` serializes a message
containing the CLIENT_CERTIFICATE_URL extension.
"""
record = ClientHello.from_bytes(
self.client_hello_packet_with_client_certificate_url_extension
)
assert (record.as_bytes() ==
self.client_hello_packet_with_client_certificate_url_extension)

def test_as_bytes_unsupported_extension(self):
"""
:func:`ClientHello.as_bytes` fails to serialize a message that
Expand Down
115 changes: 112 additions & 3 deletions tls/test/test_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@

from tls._common import enums

from tls.exceptions import InvalidPaddingException

from tls.hello_message import ClientHello, ProtocolVersion, ServerHello

from tls.message import (ASN1Cert, Certificate, CertificateRequest,
Finished, Handshake, HelloRequest,
PreMasterSecret, ServerDHParams,
ServerHelloDone)
CertificateURL, Finished, Handshake, HelloRequest,
PreMasterSecret, ServerDHParams, ServerHelloDone,
URLAndHash)


class TestCertificateRequestParsing(object):
Expand Down Expand Up @@ -283,6 +285,73 @@ def test_as_bytes_too_long(self):
certificate.as_bytes()


class TestCertificateURLParsing(object):
"""
Tests for parsing of :py:class:`tls.message.CertificateURL` messages.
"""
url_and_hash_list_bytes = (
b'\x00\x10' # url length
b'cert.example.com' # url
b'\x01' # padding
b'abcdefghijklmnopqrst' # SHA1Hash[20]
)

certificate_url_packet = (
b'\x00' # CertChainType
b'\x00\x27' # url_and_hash_list length
) + url_and_hash_list_bytes

def test_parse_certificate_url(self):
"""
:py:meth:`tls.message.CertificateURL.from_bytes` parses a valid
packet.
"""
record = CertificateURL.from_bytes(self.certificate_url_packet)
assert isinstance(record, CertificateURL)
assert record.type == enums.CertChainType.INDIVIDUAL_CERTS
assert len(record.url_and_hash_list) == 1
assert record.url_and_hash_list[0].url == b'cert.example.com'
assert record.url_and_hash_list[0].padding == 1
assert record.url_and_hash_list[0].sha1_hash == b'abcdefghijklmnopqrst'

def test_incorrect_padding(self):
"""
:py:meth:`tls.message.CertificateURL.from_bytes` rejects a packet
whose ``padding`` is not 1.
"""
bad_padding_bytes = (
b'\x00\x10' # url length
b'cert.example.com' # url
b'\x03' # padding
b'abcdefghijklmnopqrst' # SHA1Hash[20]
)
certificate_url_packet = (
b'\x00' # CertChainType
b'\x00\x27' # url_and_hash_list length
) + bad_padding_bytes

with pytest.raises(InvalidPaddingException):
CertificateURL.from_bytes(certificate_url_packet)

def test_as_bytes(self):
"""
:py:meth:`tls.message.CertificateUrl.as_bytes` returns a valid
packet.
"""
record = CertificateURL.from_bytes(self.certificate_url_packet)
assert record.as_bytes() == self.certificate_url_packet

def test_as_bytes_with_bad_padding(self):
"""
:py:meth:`tls.message.CertificateURL.as_bytes` fails to serialize a
record whose ``padding`` is not 1.
"""
record = CertificateURL.from_bytes(self.certificate_url_packet)
record.url_and_hash_list[0].padding = 5
with pytest.raises(InvalidPaddingException):
record.as_bytes()


class TestHandshakeStructParsing(object):
"""
Tests for parsing of :py:class:`tls.messages.Handshake` structs.
Expand Down Expand Up @@ -387,6 +456,20 @@ class TestHandshakeStructParsing(object):
b'some-encrypted-bytes'
)

certificate_url_packet = (
b'\x00' # CertChainType
b'\x00\x27' # url_and_hash_list length
b'\x00\x10' # url length
b'cert.example.com' # url
b'\x01' # padding
b'abcdefghijklmnopqrst' # SHA1Hash[20]
)

certificate_url_handshake_packet = (
b'\x15'
b'\x00\x00\x2a'
) + certificate_url_packet

def test_parse_client_hello_in_handshake(self):
record = Handshake.from_bytes(self.client_hello_handshake_packet)
assert isinstance(record, Handshake)
Expand Down Expand Up @@ -477,3 +560,29 @@ def test_as_bytes_server_hello_done(self):
def test_as_bytes_finished(self):
record = Handshake.from_bytes(self.finished_handshake)
assert record.as_bytes() == self.finished_handshake

def test_from_bytes_certificate_url(self):
"""
:py:class:`tls.messages.Handshake` parses a valid packet with a
``CertificateURL`` message.
"""
record = Handshake.from_bytes(self.certificate_url_handshake_packet)
assert isinstance(record, Handshake)
assert record.msg_type == enums.HandshakeType.CERTIFICATE_URL
assert record.length == 42
assert isinstance(record.body, CertificateURL)
assert record.body.type == enums.CertChainType.INDIVIDUAL_CERTS
assert len(record.body.url_and_hash_list) == 1
assert isinstance(record.body.url_and_hash_list[0], URLAndHash)
assert record.body.url_and_hash_list[0].url == b'cert.example.com'
assert record.body.url_and_hash_list[0].padding == 1
assert (record.body.url_and_hash_list[0].sha1_hash ==
b'abcdefghijklmnopqrst')

def test_as_bytes_certificate_url(self):
"""
:py:meth:`tls.message.Handshake.as_bytes` returns a valid packet when
the body contains a ``CertificateURL`` message.
"""
record = Handshake.from_bytes(self.certificate_url_handshake_packet)
assert record.as_bytes() == self.certificate_url_handshake_packet