-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Conversation
|
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No space before generator
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
|
It seems that all the tests that have failed in this PR are not caused by the changes. Are the |
|
No, not sure what's going on there just yet... |
|
Jenkins, retest this please. |
1 similar comment
|
Jenkins, retest this please. |
|
I can't understand why my patch fail the coverage tests. If anyone can help me understand I would appreciate it. |
|
The data out of codecov is making no sense here, I wouldn't worry about it for now. |
| BIGNUM *q; | ||
| BIGNUM *j; | ||
| ...; | ||
| } DH; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done :)
|
There's merge conflicts here :-( |
|
Fixed the merge conflicts :-) |
| assert res == 1 | ||
|
|
||
| if codes[0] != 0: | ||
| raise ValueError("DH private numbers did not pass safety checks.") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
|
Since this was based off an old PR it has the old |
|
I changed the PR to the |
| self.openssl_assert(res == 1) | ||
|
|
||
| codes = self._ffi.new("int[]", 1) | ||
| res = self._lib.DH_check(dh_cdata, codes) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this 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) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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:] == ( |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 **") |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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 = [( |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
jenkins, retest this please |
|
(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. | |||
There was a problem hiding this comment.
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.
There was a problem hiding this 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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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"); | |||
There was a problem hiding this comment.
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.
|
Thanks for the CR @reaperhulk I just hope that it will be ready in time for 1.7 :) |
|
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. 🎉 |
|
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. |
|
😃😃😃 |
This PR is mainly based on PR #1721.
I made the following changes:
DHPrivateKeyhas anexchangemethod that receives aDHPublicKey.default_backendsupport allDHBackendmethods.