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

Point extension #517

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

Conversation

gstarovo
Copy link
Contributor

@gstarovo gstarovo commented Apr 12, 2024

fixes #103


This change is Reviewable

Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

we should have test coverage to verify that

  1. it is actually negotiated (compressed representation is used)
  2. that if one side doesn't advertise support for compressed, that uncompressed is used

Reviewed 5 of 5 files at r1, 1 of 1 files at r2, all commit messages.
Reviewable status: all files reviewed, 22 unresolved discussions (waiting on @gstarovo)


.github/workflows/ci.yml line 8 at r1 (raw file):

    - master
    - tlslite-ng-0.7
    - format_extension

we don't need this here, please squash it with the second commit


tlslite/handshakesettings.py line 411 at r1 (raw file):

        self.keyExchangeNames = list(KEY_EXCHANGE_NAMES)
        self.cipherImplementations = list(CIPHER_IMPLEMENTATIONS)
        self.ecPointFormats = [ECPointFormat.uncompressed, 

nit: trailing whitespace

also, all new class fields should use snake_case not camelCase


tlslite/handshakesettings.py line 413 at r2 (raw file):

        self.ecPointFormats = [ECPointFormat.uncompressed, 
                               ECPointFormat.ansiX962_compressed_char2,
                               ECPointFormat.ansiX962_compressed_prime]

the values should be copied and validated that they are recognised, check how other settings are handled


tlslite/keyexchange.py line 707 at r1 (raw file):

        kex = ECDHKeyExchange(self.group_id, self.serverHello.server_version)
        self.ecdhXs = kex.get_random_private_key()
        ext_negotiated = 0

this shouldn't be a 0, it should be ECPointFormat.uncompressed to document that this is the fallback value


tlslite/keyexchange.py line 714 at r1 (raw file):

            for ext in ext_c.formats:
                if ext in ext_s.formats and ext_negotiated is None:
                    ext_negotiated = ext
  1. we don't need to set ext_negotiated to None
  2. we're looking for first match, so after we find a value that is in both, we should break from the loop

tlslite/keyexchange.py line 734 at r1 (raw file):

        kex = ECDHKeyExchange(self.group_id, self.serverHello.server_version)
        ext_supported = [0]

we should be explicit that this is ECPointFormat.uncompressed


tlslite/keyexchange.py line 761 at r1 (raw file):

        ecdhXc = kex.get_random_private_key()
        ext_negotiated = 0
        ext_supported = [0]

same here, please use ECPointFormat.uncompressed


tlslite/keyexchange.py line 767 at r1 (raw file):

            ext_supported = []
            for ext in ext_c.formats:
                ext_negotiated = None

shouldn't this be outside the loop?


tlslite/keyexchange.py line 957 at r1 (raw file):

        return bytesToNumber(getRandomBytes(needed_bytes))

    def calc_public_value(self, private, frm_negotiated=None):

the doc string should explain that this is added here for API compat and has no effect on FFDH


tlslite/keyexchange.py line 981 at r1 (raw file):

        return bytesToNumber(peer_share)

    def calc_shared_key(self, private, peer_share, frm_supported_=None):

doc string should explain that this is added for API compatibility, not because it's needed for FFDH


tlslite/keyexchange.py line 1040 at r1 (raw file):

    @staticmethod
    def _get_ext_name(ext):
        """Get extension name from the numeric value."""

it's not "extension name", it's "point format"?


tlslite/keyexchange.py line 1041 at r1 (raw file):

    def _get_ext_name(ext):
        """Get extension name from the numeric value."""
        transform = {0: 'uncompressed', 1: 'compressed', 2: 'compressed'}

here too it would be better to use defines from ECPointFormat instead of magic values


tlslite/keyexchange.py line 1046 at r1 (raw file):

    def calc_public_value(self, private, frm_negotiated=0):
        """Calculate public value for given private key."""
        extension = self._get_ext_name(frm_negotiated)

I think point_fmt would work better here as the variable name


tlslite/keyexchange.py line 1057 at r1 (raw file):

            return bytearray(point.to_bytes(encoding=extension))

    def calc_shared_key(self, private, peer_share, frm_supported_=set([0])):

why _ at the end of frm_supported_?

also, the parameters should be documented in the doc string


tlslite/tlsconnection.py line 748 at r1 (raw file):

                group_id = getattr(GroupName, group_name)
                key_share = self._genKeyShareEntry(group_id, (3, 4),
                                                   settings.ecPointFormats[0])

why this is needed?


tlslite/tlsconnection.py line 766 at r1 (raw file):

            groups.extend(self._curveNamesToList(settings))
            extensions.append(ECPointFormatsExtension().\
                              create(settings.ecPointFormats))

if the list is empty, we shouldn't send an extensions


tlslite/tlsconnection.py line 987 at r1 (raw file):

                            ext_negotiated = ext
                key_share = self._genKeyShareEntry(group_id, (3, 4),
                                                   ext_negotiated)

key shares are TLS 1.2 specific, they don't use point format negotiation

see section 4.2.8.2 of RFC 8446:

   Note: Versions of TLS prior to 1.3 permitted point format
   negotiation; TLS 1.3 removes this feature in favor of a single point
   format for each curve.

tlslite/tlsconnection.py line 1187 at r1 (raw file):

    @classmethod
    def _genKeyShareEntry(cls, group, version, ext_negotiated):

same here, not needed


tlslite/tlsconnection.py line 1224 at r1 (raw file):

                                                   "advertised group.")
            kex = self._getKEX(sr_kex.group, self.version)
            ext_supported = set([0])

please us a define


tlslite/tlsconnection.py line 2731 at r1 (raw file):

            ext_negotiated = 0
            ext_supported = set([0])

please use defines


tlslite/tlsconnection.py line 2745 at r1 (raw file):

                        "No negotiated point extension")

            key_share = self._genKeyShareEntry(selected_group,

again, key shares are tls 1.3 specific


unit_tests/test_tlslite_keyexchange.py line 2532 at r1 (raw file):

        with self.assertRaises(NotImplementedError):
            kex.calc_shared_key(None, None, None)

those changes shouldn't be needed, the code should work the same as it did before introduction of the options

Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

Reviewed 9 of 9 files at r3, all commit messages.
Reviewable status: all files reviewed, 17 unresolved discussions (waiting on @gstarovo)


scripts/tls.py line 420 at r3 (raw file):

                                for item in cipher.split(',')]
    # CHANGED
    settings.ec_point_formats = []

this seems like debugging code we shouldn't be including...


scripts/tls.py line 575 at r3 (raw file):

    # CHANGED

    settings.ec_point_formats = [2, 0]

same here, looks like debugging code to me...


tests/tlstest.py line 312 at r3 (raw file):

    connection.handshakeClientCert(settings=settings)
    testConnClient(connection)
    assert connection.session.ec_point_format == ECPointFormat.ansiX962_compressed_char2

This is actually incorrect: secp256r1, secp384r1 and secp521r1 can use either uncompressed or ansiX962_compressed_prime, they can't use ansiX962_compressed_char2

the char2 encoding is for curves like the sect163r2or the sect233r1, but don't bother searching for them: we don't have support for "characteristic-2" curves, only for "prime field" curves


tlslite/handshakesettings.py line 361 at r3 (raw file):

    :vartype ec_point_formats: list
    :ivat ec_point_formats: Enabeled point format extension for

nit: Enabled
nit: :ivar


tlslite/handshakesettings.py line 609 at r3 (raw file):

                not 64 <= other.record_size_limit <= 2**14 + 1:
            raise ValueError("record_size_limit cannot exceed 2**14+1 bytes")
        

nit: whitespace


tlslite/handshakesettings.py line 613 at r3 (raw file):

                      i not in EC_POINT_FORMATS]
        if bad_ec_ext:
            raise ValueError("Unknown ec point format extension: "

not a fan of phrasing it like this... maybe "Unknown EC point formats provided: {0}" ?


tlslite/session.py line 78 at r3 (raw file):

    :vartype ec_point_format: int
    :ivar ec_point_format: used ec point extension format; 
        created for testing

don't think we need the last part; but I think I'd rephrase it to something like: "Used EC point format for the ECDH key exchange"


tlslite/tlsconnection.py line 663 at r3 (raw file):

        if ext_c and ext_s:
            ext_ec_point = [i for i in ext_c.formats \
                        if i in ext_s.formats][0]

If we go for iterators, then probably we should go for a solution that uses them fully, something like:

ext_ec_point = next((i for i in ext_c.formats if i in ext_s.formats)

and speaking of corner cases: we probably should handle the situation when there is no overlap: that's a protocol violation, but we need to detect it and handle correctly


tlslite/tlsconnection.py line 776 at r3 (raw file):

            else:
                extensions.append(ECPointFormatsExtension().\
                                create(list([ECPointFormat.uncompressed])))

this will still send the extension... why?


tlslite/tlsconnection.py line 2287 at r3 (raw file):

            else:
                extensions.append(ECPointFormatsExtension().\
                                create(list([ECPointFormat.uncompressed])))

didn't we agree that if the list is empty, the extension should be omitted?


tlslite/tlsconnection.py line 2432 at r3 (raw file):

        ext_ec_point = ECPointFormat.uncompressed
        if ext_c and ext_s:
            ext_ec_point = [i for i in ext_c.formats if i in ext_s.formats][0]

same here, next() will be more pythonic


unit_tests/test_tlslite_keyexchange.py line 23 at r3 (raw file):

from tlslite.constants import CipherSuite, CertificateType, AlertDescription, \
        HashAlgorithm, SignatureAlgorithm, GroupName, ECCurveType, \
        SignatureScheme, ECPointFormat

unused?


unit_tests/test_tlslite_keyexchange.py line 37 at r3 (raw file):

from tlslite import VerifierDB
from tlslite.extensions import SupportedGroupsExtension, SNIExtension, \
        ECPointFormatsExtension

unused?

Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

Reviewed 6 of 6 files at r4, all commit messages.
Reviewable status: all files reviewed, 9 unresolved discussions (waiting on @gstarovo)


tests/tlstest.py line 310 at r4 (raw file):

    settings.maxVersion = (3, 3)
    settings.eccCurves = ["secp256r1", "secp384r1", "secp521r1", "x25519", "x448"]
    settings.ec_point_formats = [ECPointFormat.ansiX962_compressed_prime, ECPointFormat.uncompressed]

why we need to set it? shouldn't this be the default?


tests/tlstest.py line 2217 at r4 (raw file):

    settings.maxVersion = (3, 3)
    settings.eccCurves = ["secp256r1", "secp384r1", "secp521r1", "x25519", "x448"]
    settings.ec_point_formats = [ECPointFormat.ansiX962_compressed_prime, ECPointFormat.uncompressed]

same here, this should be the default...


tests/tlstest.py line 2221 at r4 (raw file):

                            privateKey=x509ecdsaKey, settings=settings)
    testConnServer(connection)
    print(connection.session.ec_point_format)

leftover debug ?


tlslite/tlsconnection.py line 664 at r4 (raw file):

            ext_ec_point = next((i for i in ext_c.formats \
                                 if i in ext_s.formats), \
                                    ECPointFormat.uncompressed)

no, this will just use uncompressed when there's no overlap, we can't do it like this, when there's no overlap we need to send an illegal_parameter alert


tlslite/tlsconnection.py line 2429 at r4 (raw file):

            ext_ec_point = next((i for i in ext_c.formats \
                                 if i in ext_s.formats),\
                                      ECPointFormat.uncompressed)

same here, we need to abort in case of no overlap

@tomato42
Copy link
Member

failure in test (py3.6 with tackpy, ubuntu-20.04, 3.6, tackpy) seems to be relevant

@gstarovo
Copy link
Contributor Author

failure in test (py3.6 with tackpy, ubuntu-20.04, 3.6, tackpy) seems to be relevant

Yes, yes, I'm trying to understand why it is only with Python 3.6, or maybe the problem is somewhere else.

@tomato42
Copy link
Member

I don't think it's py3.6 related, I think it's tackpy related

@tomato42
Copy link
Member

line too long errors from codeclimate are valid

Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

Reviewed 5 of 7 files at r7, 1 of 1 files at r8, 1 of 1 files at r9, 1 of 1 files at r10, all commit messages.
Reviewable status: all files reviewed, 5 unresolved discussions (waiting on @gstarovo)


tlslite/keyexchange.py line 1040 at r1 (raw file):

Previously, tomato42 (Hubert Kario) wrote…

it's not "extension name", it's "point format"?

this doesn't look to be fixed...


tlslite/keyexchange.py line 1114 at r10 (raw file):

            ecdh.load_received_public_key_bytes(peer_share,
                                                valid_encodings= \
                                                    valid_encodings)

nit: as codeclimate indicates: over indented (also, the \ is not necessary inside ()


tlslite/tlsconnection.py line 667 at r10 (raw file):

                    ext_ec_point = next((i for i in ext_c.formats \
                                        if i in ext_s.formats))
                

nit: leftover whitespace


tlslite/tlsconnection.py line 2440 at r10 (raw file):

                    ext_ec_point = next((i for i in ext_c.formats \
                                        if i in ext_s.formats))
                

nit: leftover whitespace

Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

the code itself is fine, but as it is now, it breaks ECDH in tlsfuzzer (as we've discussed in person), so technically it's incomplete

Reviewed 2 of 2 files at r11, all commit messages.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @gstarovo)

tomato42
tomato42 previously approved these changes Jun 12, 2024
Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

Reviewed 2 of 2 files at r12, all commit messages.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @gstarovo)

@tomato42
Copy link
Member

@gstarovo that fixes the tlsfuzzer issue? should I try it and if it works, merge this PR, or do you want to to some additional tweaks?

@tomato42
Copy link
Member

line too long issues from codeclimate are valid

Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

Reviewed 3 of 4 files at r13, 1 of 1 files at r14, all commit messages.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @gstarovo)

Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

Reviewable status: all files reviewed, 2 unresolved discussions (waiting on @gstarovo)


tlslite/keyexchange.py line 1065 at r14 (raw file):

    def calc_public_value(self,
                          private,
                          frm_negotiated=ECPointFormat.uncompressed):

sorry for rejecting it this late, but I've been working on point format handling for TLS 1.3 post-quantum hybrid key shares, and I've noticed that for test coverage, we need to be able to both send the shares in all the different formats (including the ones that aren't specified in TLS, like "raw" and "hybrid"), and similarly reject formats we don't want (and default to accepting only uncompressed)

so I think doing it like this: a9af7a3 will be cleaner


tlslite/keyexchange.py line 1079 at r14 (raw file):

    def calc_shared_key(self, private, peer_share,
                        frm_supported=set([ECPointFormat.uncompressed])):

as in the other comment: a list/set of strings is better here, not TLS identifiers

@gstarovo gstarovo force-pushed the point_extension branch 2 times, most recently from b1de13b to cc82cfe Compare October 22, 2024 13:50
@gstarovo gstarovo force-pushed the point_extension branch 2 times, most recently from 1a2a156 to aeb79a3 Compare October 23, 2024 09:39
Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

Reviewed 7 of 7 files at r15, all commit messages.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on @gstarovo)


tlslite/keyexchange.py line 1136 at r15 (raw file):

            raise TLSIllegalParameterException("Invalid ECC point")
        except DecodeError:
            raise TLSDecodeError("Unexpected error")

If the doc above is an indication, this should be something like "Empty point format extension"?

Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

Reviewed 1 of 1 files at r16, 1 of 1 files at r17, all commit messages.
Reviewable status: all files reviewed, 4 unresolved discussions (waiting on @gstarovo)


tlslite/keyexchange.py line 717 at r16 (raw file):

        if ext_c:
            if ext_c.formats == []:
                raise TLSDecodeError("The compression list is empty.")

I think something like "Point formats extension is empty." would be more descriptive


tlslite/tlsconnection.py line 4414 at r17 (raw file):

        except DecodeError as alert:
            for result in self._sendError(
                    AlertDescription.illegal_parameter,

either this should be decode_error or the DecodeError exception should have been caught earlier and turned into TLSIllegalParameterException...

what's the situation that leads to this code?


tlslite/tlsconnection.py line 4420 at r17 (raw file):

            alert = Alert().create(AlertDescription.illegal_parameter,
                                           AlertLevel.fatal)
            for result in self._sendMsg(alert):

why it's using _sendMsg not _sendError?


tlslite/tlsconnection.py line 4426 at r17 (raw file):

            alert = Alert().create(AlertDescription.decode_error,
                                           AlertLevel.fatal)
            for result in self._sendMsg(alert):

same here, wh not _sendError?

Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

Reviewed 2 of 2 files at r19, all commit messages.
Reviewable status: all files reviewed, 2 unresolved discussions (waiting on @gstarovo)


tlslite/keyexchange.py line 717 at r16 (raw file):

Previously, tomato42 (Alicja Kario) wrote…

I think something like "Point formats extension is empty." would be more descriptive

sorry, looks like wasn't clear: sending decode_error alert in such case is correct, the issue was only with the message associated with it


tlslite/keyexchange.py line 1138 at r19 (raw file):

            raise TLSIllegalParameterException("Invalid ECC point")
        except DecodeError:
            raise TLSIllegalParameterException("Empty point format extension")

same here, I think that TLSDecodeError was correct before

https://www.rfc-editor.org/rfc/rfc8422.html#section-5.1.2 specifies it as:

           enum {
               uncompressed (0),
               deprecated (1..2),
               reserved (248..255)
           } ECPointFormat;
           struct {
               ECPointFormat ec_point_format_list<1..2^8-1>
           } ECPointFormatList;

so an empty one should result in a decode_error not illegal_parameter

tomato42
tomato42 previously approved these changes Nov 12, 2024
Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

Reviewed 2 of 2 files at r20, all commit messages.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @gstarovo)

check if the client adverties the uncompressed point format extension

error when uncompressed is not supported

fix: changes in accepting the format(form ECPointFormat to string) of ec format.

fix: tests, keyShares added to tests for checking ecc point extension
…ormat is not found; alert decode_error is added when the list of ecc extenison is empty

fix: message alert description
… to check of client hello by server; that way the server will abort the connection after bad client hello, not after server key exchange
Copy link
Member

@tomato42 tomato42 left a comment

Choose a reason for hiding this comment

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

side note: I have no problem with fixup commits, but the CI will verify that the test coverage passes after each and every one of them, so it may be a good idea to rebase and apply those fixup patches to the base commit that they're changing

Reviewed 3 of 5 files at r21, 2 of 2 files at r25, all commit messages.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on @gstarovo)


tlslite/tlsconnection.py line 3441 at r25 (raw file):

        ecExt = clientHello.getExtension(ExtensionType.ec_point_formats)
        if ecExt:
            if ecExt.formats == []:

emptiness of a collection in python is generally tested by using if not ecExt.formats:

@tomato42
Copy link
Member

...and the CI failures seem to be related to your changes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for alternative ECC point formats
2 participants