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 all 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
1 change: 1 addition & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Submodules
Subpackages
ciphersuites
prepending
subconstruct
subconstructs
versa
prf
Expand Down
35 changes: 35 additions & 0 deletions tls/_common/_constructs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import six

from tls.exceptions import TLSValidationException


class _UBInt24(construct.Adapter):
def _encode(self, obj, context):
Expand Down Expand Up @@ -211,6 +213,39 @@ def EnumSwitch(type_field, type_enum, value_field, value_choices, # noqa
default=default))


class TLSExprValidator(construct.Validator):
Copy link
Member

Choose a reason for hiding this comment

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

This is a great way to implement TLSOneOf! We can reuse TLSExprValidator for other constructs. Thanks!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

:)

Took this idea from construct's OneOf.

"""
Like :py:class:`construct.ExprValidator`, but raises a
:py:class:`tls.exceptions.TLSValidationException` on validation failure.

This is necessary because any ConstructError signifies the end of
subconstruct repetition to Range, which in turn breaks use with
``TLSPrefixedArray``.
"""
def __init__(self, subcon, validator):
super(TLSExprValidator, self).__init__(subcon)
self._validate = validator

def _decode(self, obj, context):
if not self._validate(obj, context):
raise TLSValidationException("object failed validation", obj)
return obj


def TLSOneOf(subcon, valids): # noqa
"""
Validates that the object is one of the listed values, both during parsing
and building. Like :py:meth:`construct.OneOf`, but raises a
:py:class:`tls.exceptions.TLSValidationException` instead of a
``ConstructError`` subclass on mismatch.

This is necessary because any ConstructError signifies the end of
Copy link
Member

Choose a reason for hiding this comment

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

It might be nice to put an explanation like this into TLSExprValidator's doc string.

subconstruct repetition to Range, which in turn breaks use with
``TLSPrefixedArray``.
"""
return TLSExprValidator(subcon, lambda obj, ctx: obj in valids)


class SizeAtLeast(construct.Validator):
"""
A :py:class:`construct.adapter.Validator` that validates a
Expand Down
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
116 changes: 111 additions & 5 deletions tls/_common/test/test_constructs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@

import pytest

from tls._common._constructs import (BytesAdapter, EnumClass,
EnumSwitch, Opaque,
PrefixedBytes, SizeAtLeast,
from tls._common._constructs import (BytesAdapter, EnumClass, EnumSwitch,
Opaque, PrefixedBytes, SizeAtLeast,
SizeAtMost, SizeWithin,
TLSPrefixedArray, UBInt24,
_UBInt24)
TLSExprValidator, TLSOneOf,
TLSPrefixedArray, UBInt24, _UBInt24)

from tls.exceptions import TLSValidationException


@pytest.mark.parametrize("byte,number", [
Expand Down Expand Up @@ -94,6 +95,111 @@ def test_decode_passes_value_through(self, bytes_adapted, value):
assert bytes_adapted._decode(value, context=object()) is value


class TestTLSExprValidator(object):
"""
Tests for :py:class:`tls._common._constructs.TLSExprValidator`.
"""
@pytest.fixture
def data_class(self):
Copy link
Member

Choose a reason for hiding this comment

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

Good name - no need for # noqa. I think we should use this approach more!

"""
A :py:func:`construct.macros.UBInt8` construct that requires the
input value to be equal to 6.
"""
return TLSExprValidator(UBInt8('input_byte'),
lambda obj, ctx: obj == 6)

def test_parse_invalid(self, data_class):
"""
:py:class:`tls.common._constructs.TLSExprValidator` raises a
``TLSValidationException`` when parsing a value that does not
evaluate to the provided expression.
"""
with pytest.raises(TLSValidationException):
data_class.parse(b'\xff')

def test_parse_valid(self, data_class):
"""
:py:class:`tls.common._constructs.TLSExprValidator` parses a value
that evaluates to the provided expression.
"""
assert data_class.parse(b'\x06') == 6

def test_build_invalid(self, data_class):
"""
:py:class:`tls.common._constructs.TLSExprValidator` raises a
``TLSValidationException`` when serializing a value that does not
evaluate to the provided expression.
"""
with pytest.raises(TLSValidationException):
data_class.build(2)

def test_build_valid(self, data_class):
"""
:py:class:`tls.common._construct.TLSExprValidator` successfully
serializes a value into bytes when it evaluates to the provided
expression.
"""
assert data_class.build(6) == b'\x06'


class TestTLSOneOf(object):
"""
Tests for :py:meth:`tls._common._constructs.TLSOneOf`.
"""

@pytest.fixture
def data_class(self):
"""
A :py:func:`construct.macros.UBInt8` construct that requires the
input value to be equal to one of 1, 3, or 5.
"""
return TLSOneOf(UBInt8('input'),
[1, 3, 5])

def test_parse_invalid(self, data_class):
"""
:py:meth:`tls.common._constructs.TLSOneOf` raises a
``TLSValidationException`` when parsing a value that is not one of
the values in the provided list.
"""
with pytest.raises(TLSValidationException):
data_class.parse(b'\xff')

@pytest.mark.parametrize('input_bytes,parsed_output', [
(b'\x01', 1),
(b'\x03', 3),
(b'\x05', 5),
])
def test_parse_valid(self, data_class, input_bytes, parsed_output):
"""
:py:meth:`tls.common._constructs.TLSOneOf` parses a value that
equals one of the values in the provided list.
"""
assert data_class.parse(input_bytes) == parsed_output

def test_build_invalid(self, data_class):
"""
:py:meth:`tls.common._constructs.TLSOneOf` raises a
``TLSValidationException`` when serializing a value that is not one
of the values in the provided list.
"""
with pytest.raises(TLSValidationException):
data_class.build(2)

@pytest.mark.parametrize('input,built_bytes', [
(1, b'\x01'),
(3, b'\x03'),
(5, b'\x05'),
])
def test_build_valid(self, data_class, input, built_bytes):
"""
:py:meth:`tls.common._construct.TLSOneOf` successfully serializes a
value into bytes when it evaluates to one of the values in the
provided list.
"""
assert data_class.build(input) == built_bytes


@pytest.mark.parametrize("bytestring,encoded", [
(b"", b"\x00" + b""),
(b"some value", b"\x0A" + b"some value"),
Expand Down
29 changes: 27 additions & 2 deletions tls/_constructs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@

from functools import partial

from construct import (Array, Bytes, Pass, Struct, Switch, UBInt16, UBInt32,
from construct import (Array, Bytes, Pass, Struct,
Switch, UBInt16, UBInt32,
UBInt8)

from tls._common import enums

from tls._common._constructs import (EnumClass, EnumSwitch, Opaque,
PrefixedBytes, SizeAtLeast, SizeAtMost,
SizeWithin, TLSPrefixedArray, UBInt24)
SizeWithin, TLSOneOf, TLSPrefixedArray,
UBInt24)

from tls.ciphersuites import CipherSuites

Expand Down Expand Up @@ -84,6 +86,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 +114,9 @@
enums.ExtensionType.SIGNATURE_ALGORITHMS: Opaque(
SupportedSignatureAlgorithms
),
enums.ExtensionType.CLIENT_CERTIFICATE_URL: Opaque(
ClientCertificateURL
),
},
default=Pass,
)
Expand Down Expand Up @@ -195,3 +205,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),
TLSOneOf(UBInt8('padding'), [1]),
Bytes("sha1_hash", 20),
)

CertificateURL = Struct(
"CertificateURL",
EnumClass(UBInt8("type"), enums.CertChainType),
TLSPrefixedArray("url_and_hash_list", URLAndHash),
)
16 changes: 14 additions & 2 deletions tls/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,21 @@
from __future__ import absolute_import, division, print_function


class UnsupportedCipherException(Exception):
class TLSException(Exception):
"""
This is the root exception from which all other exceptions inherit.
Lower-level parsing code raises very specific exceptions that higher-level
code can catch with this exception.
"""


class UnsupportedCipherException(TLSException):
pass


class UnsupportedExtensionException(TLSException):
pass


class UnsupportedExtensionException(Exception):
class TLSValidationException(TLSException):
pass
61 changes: 59 additions & 2 deletions tls/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,59 @@ 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()


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

def as_bytes(self):
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)
return cls(
type=construct.type,
url_and_hash_list=[
URLAndHash(
url=url_and_hash.url,
padding=url_and_hash.padding,
sha1_hash=url_and_hash.sha1_hash,
)
for url_and_hash in construct.url_and_hash_list],
)


@attr.s
class Finished(object):
verify_data = attr.ib()
Expand All @@ -230,7 +283,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 +325,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 +338,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
Loading