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

OpenSSL DH backend implementation [Second attempt] #2914

Merged
merged 55 commits into from
Nov 25, 2016

Conversation

palaviv
Copy link
Contributor

@palaviv palaviv commented May 20, 2016

This PR is mainly based on PR #1721.
I made the following changes:

  • The key exchange API is very similar to the ECDH API. The DHPrivateKey has an exchange method that receives a DHPublicKey.
  • The default_backend support all DHBackend methods.

@alex
Copy link
Member

alex commented May 20, 2016

Jenkins, ok to test.


def generate_dh_private_key_and_parameters(self, generator, key_size):
for b in self._filtered_backends(DHBackend):
return b.generate_dh_private_key_and_parameters( generator, key_size)
Copy link
Member

Choose a reason for hiding this comment

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

No space before generator

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@palaviv
Copy link
Contributor Author

palaviv commented May 23, 2016

It seems that all the tests that have failed in this PR are not caused by the changes. Are the InvocationError known issue?

@reaperhulk
Copy link
Member

No, not sure what's going on there just yet...

@reaperhulk
Copy link
Member

Jenkins, retest this please.

1 similar comment
@alex
Copy link
Member

alex commented May 29, 2016

Jenkins, retest this please.

@palaviv
Copy link
Contributor Author

palaviv commented Jun 3, 2016

I can't understand why my patch fail the coverage tests. If anyone can help me understand I would appreciate it.

@alex
Copy link
Member

alex commented Jun 3, 2016

The data out of codecov is making no sense here, I wouldn't worry about it for now.

BIGNUM *q;
BIGNUM *j;
...;
} DH;
Copy link
Member

Choose a reason for hiding this comment

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

mmm, so this definitiion, using the struct fields, won't work in OpenSSL 1.1.0, so this will have to be ported to use the various accessors that OpenSSL exposes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In OpenSSL 1.1.0 the DH_get0_pqg, DH_set0_pqg, DH_get0_key, DH_set0_key (https://www.openssl.org/docs/manmaster/crypto/DH_get0_pqg.html) functions were added. But I think that porting this struct will be more practical then using them.

Copy link
Member

Choose a reason for hiding this comment

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

Those functions will need to be declared and used. You can see examples in the rsa bindings. Going forward all bindings need to be compatible with 1.1.0 as well as earlier versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done :)

@alex
Copy link
Member

alex commented Jun 4, 2016

There's merge conflicts here :-(

@palaviv
Copy link
Contributor Author

palaviv commented Jun 5, 2016

Fixed the merge conflicts :-)

assert res == 1

if codes[0] != 0:
raise ValueError("DH private numbers did not pass safety checks.")
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 the line missing coverage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@reaperhulk
Copy link
Member

Since this was based off an old PR it has the old assert model. Consequently all the asserts need to be replaced with calls to the openssl_assert method. You can see how this works in other parts of the openssl backend's backend.py. (Thanks for working on this)

@palaviv
Copy link
Contributor Author

palaviv commented Jun 6, 2016

I changed the PR to the openssl_assert @reaperhulk. Thank you for the CR :)

self.openssl_assert(res == 1)

codes = self._ffi.new("int[]", 1)
res = self._lib.DH_check(dh_cdata, codes)
Copy link
Member

Choose a reason for hiding this comment

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

This will fail with DH_UNABLE_TO_CHECK_GENERATOR for any generator that's not 2 or 5. However, we don't limit to 2/5 in the parameter creation, which means you can write code that fails this check like so:

params = dh.generate_parameters(7, 512, backend)
params.generate_private_key().private_numbers().private_key(backend)

To fix this let's limit the generator to 2 or 5 for now (we can revisit whether we need to change how we use DH_check at a later date if users show up asking for more generator flexibility)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

Copy link
Member

@reaperhulk reaperhulk left a comment

Choose a reason for hiding this comment

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

We're getting close now, thanks for working so hard on this PR!

:return bytes: The agreed key. The bytes are ordered in 'big' endian.


.. class:: DHPrivateNumbers(x, public_numbers)
Copy link
Member

Choose a reason for hiding this comment

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

The 3 numbers classes should be grouped under a

Numbers
~~~~~~~

section (like RSA).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

def _handle_dh_compute_key_error(errors, backend):
lib = backend._lib

backend.openssl_assert(errors[0][1:] == (
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 still an outstanding question)

buf = self._backend._ffi.new("unsigned char[]", self._key_size)
res = self._backend._lib.DH_compute_key(
buf,
self._backend._int_to_bn(peer_public_key.public_numbers().y),
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 to get the public value using DH_get0_key in this case. No need to do BN -> int -> BN conversion.

DH_get0_key should allow you to pass null for priv_key btw

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

key = self._backend._ffi.buffer(buf)[:res]
pad = self._key_size - len(key)

if pad > 0:
Copy link
Member

Choose a reason for hiding this comment

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

DH_compute_key doesn't do this for you? (Clearly it doesn't, just want to make sure I understand why we need to do this)

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 you can see test_symmetric_key_padding that actually cover this specific flow.

dh_cdata, self._backend._lib.DH_free
)

p = self._backend._ffi.new("BIGNUM **")
Copy link
Member

Choose a reason for hiding this comment

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

DHParams_dup can give you a new DH * that already has p and g populated, which will simplify a lot of this code (you'll still need to register it for gc and set the pub_key though)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

assert backend.dh_parameters_supported(23, 5)
assert not backend.dh_parameters_supported(23, 18)

def test_convert_to_serialized(self, backend):
Copy link
Member

Choose a reason for hiding this comment

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

Could we change the tests with the name "serialized" to say "numbers" instead? In general we tend to use serialization for PEM/DER serialization to bytes rather than the *Numbers integer repr classes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

key = private.private_key(backend)
symkey = key.exchange(public.public_key(backend))

assert int_from_bytes(symkey, 'big') == int(vector["k"], 16)
Copy link
Member

Choose a reason for hiding this comment

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

A bit nitpicky, but could we have this be assert symkey == int_to_bytes(int(vector["k"], 16))? I haven't tested that so please confirm I did it right!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can do this but I think this way is better because of the padding. In case that any of the vectors need padding your test will fail because the symkey buffer is padded.

assert len(symkey) == 512 // 8
assert symkey[:1] == b'\x00'

bad_tls_exchange_params = [(
Copy link
Member

Choose a reason for hiding this comment

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

Could you move these values into a vector file (with a few small comments noting in what way each one is broken)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@reaperhulk
Copy link
Member

jenkins, retest this please

@alex
Copy link
Member

alex commented Nov 22, 2016

(Moved this to the 17th release, which we hope to get done in 2-3 weeks)

@@ -0,0 +1,19 @@
# This are pairs of DH vectors that an exchange between them will result in an error.
Copy link
Member

Choose a reason for hiding this comment

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

s/this/these

Please add a comment about what type of failure each one causes and that these were custom generated.

This vector file also needs an entry in test-vectors.rst.

Copy link
Member

@reaperhulk reaperhulk left a comment

Choose a reason for hiding this comment

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

Hopefully just one last round here! Sorry for the scattershot comments, the PR is so large that I end up reviewing the entire thing over and over and I'm catching new things...

def __init__(self, backend, dh_cdata):
self._backend = backend
self._dh_cdata = dh_cdata
self._key_size = self._backend._lib.DH_size(dh_cdata)
Copy link
Member

Choose a reason for hiding this comment

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

We allow non-byte aligned bit lengths when generating parameters. DH_size returns the byte length. For key_size multiplying by 8 is not a safe assumption because p could be 1021 bits, which would show up as 128 bytes. I think we need to rename this to _key_size_bytes.


@property
def key_size(self):
return self._key_size * 8
Copy link
Member

Choose a reason for hiding this comment

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

With the change to _key_size_bytes this no longer works. But you can get the value by doing a get0_pqg with null for q and g and then calling BN_num_bits on p[0] (you can memoize this in init if you want, I don't have a preference)


def exchange(self, peer_public_key):

buf = self._backend._ffi.new("unsigned char[]", self._key_size)
Copy link
Member

Choose a reason for hiding this comment

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

This becomes _key_size_bytes

self._backend.openssl_assert(res >= 1)

key = self._backend._ffi.buffer(buf)[:res]
pad = self._key_size - len(key)
Copy link
Member

Choose a reason for hiding this comment

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

Also _key_size_bytes now. Padding should still work as expected for non-byte aligned keys since the data written to key by necessity must already be padded?

def __init__(self, backend, dh_cdata):
self._backend = backend
self._dh_cdata = dh_cdata
self._key_size = self._backend._lib.DH_size(dh_cdata) * 8
Copy link
Member

Choose a reason for hiding this comment

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

Same problem with bits vs bytes. Since this is only used for key_size I'd suggest renaming it to _key_size_bits and doing get0_pqg. You can factor this out into a function _get_dh_num_bits since we're using it in two places.

@@ -463,3 +470,4 @@ header format (substituting the correct information):
.. _`Russian CA`: https://e-trust.gosuslugi.ru/MainCA
.. _`test/evptests.txt`: https://github.com/openssl/openssl/blob/2d0b44126763f989a4cbffbffe9d0c7518158bb7/test/evptests.txt
.. _`unknown signature OID`: https://bugzilla.mozilla.org/show_bug.cgi?id=405966
.. _`botan`: https://raw.githubusercontent.com/randombit/botan/master/src/tests/data/pubkey/dh.vec
Copy link
Member

Choose a reason for hiding this comment

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

@@ -0,0 +1,182 @@
# Licensed under the Apache License, Version 2.0 (the "License");
Copy link
Member

Choose a reason for hiding this comment

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

This license needs to be replaced with the following:

# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

@palaviv
Copy link
Contributor Author

palaviv commented Nov 24, 2016

Thanks for the CR @reaperhulk I just hope that it will be ready in time for 1.7 :)

@reaperhulk
Copy link
Member

One last request @palaviv: please add a changelog entry describing the new feature! Once that's there and the tests have passed I'm ready to merge. 🎉

@reaperhulk
Copy link
Member

For the record, I have verified the vectors against the botan link in question and will do so again once the changelog commit is pushed.

@palaviv
Copy link
Contributor Author

palaviv commented Nov 25, 2016

😃😃😃

@reaperhulk reaperhulk merged commit 495f21a into pyca:master Nov 25, 2016
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 22, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Development

Successfully merging this pull request may close these issues.

None yet

5 participants