[#8966] Adding curve25519 and ed25519 #644

Open
wants to merge 105 commits into
from

Conversation

Projects
None yet
6 participants
Contributor

the0id commented Dec 23, 2016

This adds curve25519 and ed25519 to Twisted. Cryptography doesn't support 25519 yet, so pyNaCL is required.

the0id and others added some commits Jul 27, 2016

Adding in ECDH, ECDSA, and Curve25519 to Conch.
Curve25519 is not supported by Cryptography, so pynacl and curve25519-donna are required.
This has the same ECDSA issue as the other branch does that it is mostly, but not completely stable.

@the0id the0id changed the title from the0id curve25519 to Adding curve25519 and ed25519 Dec 23, 2016

codecov-io commented Dec 23, 2016

Codecov Report

Merging #644 into trunk will decrease coverage by 0.37%.
The diff coverage is 100%.

@@            Coverage Diff             @@
##            trunk     #644      +/-   ##
==========================================
- Coverage    91.3%   90.93%   -0.38%     
==========================================
  Files         839      839              
  Lines      148399   148609     +210     
  Branches    12970    13006      +36     
==========================================
- Hits       135502   135141     -361     
- Misses      10821    11356     +535     
- Partials     2076     2112      +36

@the0id the0id changed the title from Adding curve25519 and ed25519 to [#8966] Adding curve25519 and ed25519 Dec 23, 2016

the0id added some commits Jan 3, 2017

the0id added some commits Jan 17, 2017

Adding more unit tests, removing some redundant checks, and renaming …
…the ed25519 keys in keydata to make txchecker happy for now.
Petting codecov on the head and whispering that it's ok if there's no…
… extra value when printing ed25519 keys.

Thanks for your continued work on Conch's cipher suites.

We should discuss the results of this review, but my general feeling here is that even if the dependency isn't optional, we are 4 mostly copy/pasted algorithms in to needing to refactor the way that conch deals with curves. I think this one might be a bridge too far. Do you see an easy way to modularize? I'm OK with deferring to a subsequent ticket, but I just worry that this is becoming less and less maintainable.

Please address this feedback and resubmit.

src/twisted/conch/ssh/transport.py
@@ -19,6 +19,10 @@
from hashlib import md5, sha1, sha256, sha384, sha512
+import nacl.public
@glyph

glyph Jan 28, 2017

Owner

See comment in setup.py about being an optional dependency.

src/twisted/conch/ssh/transport.py
- int(
- binascii.hexlify(
- ecPriv.exchange(ec.ECDH(), theirECPub)), 16))
+ if self.kexAlg.find(b'25519') != -1:
@glyph

glyph Jan 28, 2017

Owner

Just a nitpick, but, this is perhaps more idiomatically expressed as if b'25519' in self.kexAlg

@the0id

the0id Jan 30, 2017

Contributor

I remember if X in Y causing problems with python3, but that seems to be incorrect and I agree if X in Y is better. Changed.

+ # Always generate new private and public keys
+ ecPriv = nacl.public.PrivateKey.generate()
+ ecPub = ecPriv.public_key
+ encPub = ecPub._public_key
@glyph

glyph Jan 28, 2017

Owner

This looks like it's reaching into the private implementation details of a dependency. Is it? If so - why does it need to?

@the0id

the0id Jan 30, 2017

Contributor

It is, and it's because of how pynacl decided to do their secret key generation. They call their method "salsa" which follows the standard way of generating a curve25519 secret key but then throws an extra transform on it. Which means to get it to work with Twisted I have to call the private method.

@glyph

glyph Feb 21, 2017

Owner

I don't quite understand; wouldn't "throwing an extra transform" on it mean that the values are different?

@the0id

the0id Feb 23, 2017

Contributor

Yup. If you use the standard method it will return a value that works for pynacl, but won't work for almost every other implementation, including OpenSSH.

Why they do this I have no idea, I'm sure they have their reasons, but this was the only way I could get it to work.

+ int(binascii.hexlify(
+ crypto_scalarmult(ecPriv._private_key,
+ theirECPub._public_key)), 16))
+ else:
@glyph

glyph Jan 28, 2017

Owner

One other nice thing about making this an optional dependency would be that we could potentially factor out this extremely long function into a couple of smaller cipher-management functions so as to avoid monolithic changes like having to indent this lengthy "else" block even though the lines aren't being touched.

+ self.curve, pubKey).public_key(
+ default_backend())
+
+ # We need to convert to hex,
@glyph

glyph Jan 28, 2017

Owner

Given that this comment is repeated twice, and now the MP(... expression is repeated 4 times, some refactoring seems to be in order.

@the0id

the0id Jan 30, 2017

Contributor

I see what you're saying, and have had similar thoughts, but I don't know if I'm convinced it's needed yet.

DH, ECDH, and curve25519 all do something like this, but each of them do it in a slightly different way. So if we were to refactor and abstract this behavior I think we'd just end up moving the same code to a different location.

@@ -84,6 +85,16 @@
'curve': b'ecdsa-sha2-nistp521'
}
+ED25519Data = {
@glyph

glyph Jan 28, 2017

Owner

I realize that this is just repeating an instance of an existing pattern, but could we replace this with something that generates a test key on the fly, rather than supplying static data? Long term, we should be deleting every static private key in this module.

@the0id

the0id Jan 30, 2017

Contributor

I don't have a strong opinion on whether or not this should be done, but I think it'd be best to do it as it's own ticket.

+ keydata.publicED25519openssh)
+
+ # We need to patch the comparison value because SecureRandom
+ # gets patched when run as a unit test.
@glyph

glyph Jan 28, 2017

Owner

How is this comparison value "patched"? Also; can this refer more specifically to what does the SecureRandom patching, rather than just alluding to it? Long-term, this is the sort of thing we should parameterize rather than patch.

@the0id

the0id Jan 30, 2017

Contributor

When trial is run, SecureRandom is patched by test_keys.py:217 to return '\xff' for each byte requested.

I wasn't sure how detailed to go into this explanation in the comment since it's only relevant when running trial. I saw it that it was more important to let a normal user know that every time this function is called the output will be slightly different.

src/twisted/python/_setup.py
@@ -96,6 +96,7 @@
'pyasn1',
'cryptography >= 0.9.1',
'appdirs >= 1.4.0',
+ 'pynacl >= 1.0.1',
@glyph

glyph Jan 28, 2017

Owner

Additional cryptographic dependencies are often met with high levels of scrutiny. If possible, I'd really like to see this implemented in a way which makes pynacl an optional dependency.

This doesn't necessarily mean that the entry in setup.py needs to go away, but it strikes me that "cryptography yes, pynacl no" might end up being a configuration someone cares about.

@glyph

glyph Jan 28, 2017

Owner

This is not mandatory feedback, feel free to push back on this, if you disagree. I am not really sure about the political environment re: nacl as a crypto lib. Perhaps @reaperhulk or @alex could opine as well, if this is a legitimate concern.

@reaperhulk

reaperhulk Jan 28, 2017

Contributor

libsodium is definitely still the "less acceptable to upper management" crypto library, but its presence here is because OpenSSH already extensively uses crypto primitives from djb and OpenSSL doesn't (yet) support them. If you want to support these things in the near term you'll need to pull in pynacl or do it in pure Python (don't do that).

@glyph

glyph Jan 28, 2017

Owner

The question is really: should Twisted be concerned about environments where someone will say "uh oh, pynacl, you can't install that, denied"? Or is this the kind of thing that will be configured at a policy level? (After all you'd need to actually use these ciphers in your deployment to exercise this code.)

@reaperhulk

reaperhulk Jan 28, 2017

Contributor

Good question. I've never been a huge fan of optional deps, but a project like twisted probably does need to care about things like that. It doesn't seem unreasonable to state that a base install of twisted conch must support RSA and NIST ecdsa (via a non-optional cryptography dep), but that users who want access to ed25519 should install pynacl. ed25519 is likely to appear in cryptography soon-ish as well, albeit only accessible to users linking against the latest OpenSSL.

@the0id

the0id Jan 30, 2017

Contributor

I have issues with how pynacl operates myself, but I found it to be the lesser evil. There are other libraries that support *25519, but some only support curve25519, some only support ed25519, and most of them haven't been updated in at least a year or two.

How I see things is that pynacl is optional to the extent that cryptography and pyasn1 are optional. They're not needed unless you want to use conch, and since *25519 is the default (and strongly encouraged) algorithm of OpenSSH it should be included when conch is.

@glyph

glyph Feb 21, 2017

Owner

@the0id I don't think you're grasping my concern here:

Let's say I work at a company that's using Twisted for SSH, but that has a heavy-weight dependency audit process for each new library that gets deployed. Now, we're still adopting new dependencies and causing more trouble for people working at such shops; however, new crypto dependencies are often given much tighter scrutiny. By making this change unconditional, I have to upgrade Twisted and go through the pynacl review process at the exact same time - rather than installing new Twisted immediately and kicking off the review process for pynacl, which, once it gets approved, gives me a new capability.

@the0id

the0id Feb 23, 2017

Contributor

I see what you're saying now, and making curve25519 an optional dependency may not be a much work as I thought. So I'll look into it.

@glyph

glyph Feb 24, 2017

Owner

I should note; it's OK to make the conch extra hard depend on this, since for most users, that should be the right thing. I'm mainly talking about leaving the code in such a state that an escape hatch like a separate conch-no25519 extra would be possiblel.

the0id added some commits Jan 30, 2017

Contributor

alex commented Apr 7, 2017

I think you have a merge issue, this appears to be readding back 1024-bit DH and a few other curves we removed.

Contributor

the0id commented Apr 7, 2017

Now that I've had some time to look at this again, making pynacl an option dependency was pretty easy. With the exception that pyflakes doesn't like how I'm importing nacl.public into _kex.py but not using it.

I see why pyflakes is objecting to this, but I'm doing this to check to see if pynacl is installed before adding curve25519 as a supported kex. I could create a nacl.public object to make pyflakes happy, but that doesn't seem like a good solution to me.

How should I go about this?

Contributor

the0id commented Apr 7, 2017

@alex yeah as with our conversation on #749 I think those curves should be in Twisted, and since they were originally included in this request I'm adding them back in.

1024-DH I put back in as well because it was also originally in this merge, and while I know it's been deprecated, it's still supported and I think it's worth supporting here as it's still widely used and I believe Twisted should be as compatible with as many applications as possible.

Contributor

the0id commented Apr 7, 2017

@alex Oh thanks for the link, I'll look into that. 👍

Contributor

alex commented Apr 7, 2017

This PR is for adding X25519 and Ed25519 support, disagreements about 1024-bit DH or other curves belong somewhere else, not here.

the0id added some commits Apr 7, 2017

Contributor

the0id commented Apr 7, 2017

@alex I guess you're right. I'm reverting those changes and will open a new PR.

the0id added some commits Apr 7, 2017

Contributor

the0id commented Apr 10, 2017

I'm not sure if my last commit made things better or worse, but the root of the problem seems to be the same.

The tests are failing because nacl isn't present. Since those tests are skipped, codecov fails. Is there a way to get TravisCI to use nacl without having it be required in _setup.py? How should I do this?

Owner

glyph commented Apr 11, 2017

My preference would be for you to add it as a new required dependency of the conch extra, but then add a new extra, conch_no_25519 or somesuch, which doesn't include pynacl. You can then test it both ways by adding tox environments which install each extra.

the0id added some commits Apr 11, 2017

Contributor

the0id commented Apr 11, 2017

@glyph I see what you're saying now. No problem, can do, as soon as I figure out how to do. :)

the0id added some commits Apr 14, 2017

Contributor

the0id commented Apr 18, 2017

I can't tell if I did this right. The new tests ran but codecov didn't.

Owner

glyph commented Apr 19, 2017

Codecov has the occasional reliability issue :-\

the0id added some commits Apr 19, 2017

Contributor

the0id commented Apr 20, 2017

I got 99 commits, so lets merge this one.

src/twisted/conch/ssh/_kex.py
@@ -126,6 +127,25 @@ class _DHGroupExchangeSHA1(object):
@implementer(_IFixedGroupKexAlgorithm)
+class _DHGroup1SHA1(object):
@alex

alex May 27, 2017

Contributor

This still needs to be removed.

src/twisted/conch/ssh/keys.py
@@ -3,7 +3,7 @@
# See LICENSE for details.
"""
-Handling of RSA, DSA, and EC keys.
+Handling of RSA, DSA, EC, and *25519 keys.
@alex

alex May 27, 2017

Contributor

This should just say Ed25519, this module doesn't do anything with X25519

src/twisted/conch/ssh/keys.py
@@ -289,6 +304,9 @@ def _fromString_PRIVATE_BLOB(cls, blob):
elif keyType == b'ssh-dss':
p, q, g, y, x, rest = common.getMP(rest, 5)
return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x)
+ elif keyType == b'ssh-ed25519':
+ pr = nacl.signing.SigningKey(common.getNS(rest)[0])
@alex

alex May 27, 2017

Contributor

The variable name pr has me a bit confused; can we pick something more intuitive?

@@ -589,7 +617,10 @@ def _guessStringType(cls, data):
return 'public_lsh'
elif data.startswith(b'('):
return 'private_lsh'
- elif data.startswith(b'\x00\x00\x00\x07ssh-') or data.startswith(b'\x00\x00\x00\x13ecdsa-'):
+ elif data.startswith(b'\x00\x00\x00\x0bssh-'): # ED25519
@alex

alex May 27, 2017

Contributor

The data will continue ssh-ed25519 right? Might be easier to read if you include that in the string.

@the0id

the0id May 31, 2017

Contributor

It will, and I see what you're saying, but I think it's better how it is. It's a little faster to process and it fits the convention of the rest of the checks. :)

@@ -786,7 +841,8 @@ def __repr__(self):
o = o[:-1]
lines.append('\t' + o)
lines[-1] = lines[-1] + '>'
- return '\n'.join(lines)
+ reprstr = '\n'.join(lines)
+ return reprstr
@alex

alex May 27, 2017

Contributor

These changes appear to be unrelated, there's nothing wrong with just return when you have the value.

@the0id

the0id May 31, 2017

Contributor

Moving the return where it is now fits in better with the block. Each conditional creates a reprstr value, with a single return of that value at the end.

src/twisted/conch/ssh/keys.py
@@ -901,7 +958,10 @@ def public(self):
@rtype: L{Key}
@return: A public key.
"""
- return Key(self._keyObject.public_key())
+ if self.type() is 'ED25519':
@alex

alex May 27, 2017

Contributor

This should not use is, it should use ==

@the0id

the0id May 31, 2017

Contributor

Good catch. 👍

+ elif isinstance(
+ self._keyObject, (nacl.signing.VerifyKey,
+ nacl.signing.SigningKey)):
+ return 'ED25519'
@alex

alex May 27, 2017

Contributor

Conventionally, it's typed Ed25519, lowercase d.

@the0id

the0id May 31, 2017

Contributor

I think it's better to leave this all caps because the rest of the return strings are in all caps.

src/twisted/conch/ssh/keys.py
-
- if self.type() == 'EC':
+ if self.type() == 'ED25519':
+ # OpenSSH loosely follows the openssh-key-v1 specification
@alex

alex May 27, 2017

Contributor

This logic should be split out into its own method for readability.

src/twisted/conch/ssh/keys.py
+ try:
+ key.verify(data, common.getNS(signature)[0])
+ return True
+ except:
@alex

alex May 27, 2017

Contributor

Catch a specific exception, not a bare catch.

src/twisted/conch/ssh/transport.py
@@ -1578,8 +1602,17 @@ def ssh_KEXINIT(self, packet):
# Connection was disconnected while doing base processing.
# Maybe no common protocols were agreed.
return
+ # Curve25519
+ if self.kexAlg.find(b'curve25519') >= 0:
@alex

alex May 27, 2017

Contributor

This can just be b'curve25519' in self.kexAlg

Contributor

alex commented May 27, 2017

There's also a merge conflict.

Contributor

rodrigc commented May 27, 2017

You need to take a merge from trunk and move your 8966.feature file to the newsfragments directory. Things have moved around due to this: #763

the0id added some commits May 31, 2017

Contributor

the0id commented May 31, 2017

@alex, @rodrigc thanks for the review. I've made some of the changes requested, and gave my reasons for not changing the others.

@rodrigc thanks for the head's up about moving the file to newsfragments. @markrwilliams helped me out with the same thing on another PR. :)

the0id added some commits Aug 18, 2017

Contributor

the0id commented Aug 22, 2017

The last two times appveyor has run two of the tests fail because openssh can't be installed.

Build started
git clone -q https://github.com/twisted/twisted.git C:\projects\twisted
git fetch -q origin +refs/pull/644/merge:
git checkout -qf FETCH_HEAD
Restoring build cache
Cache 'C:\Users\appveyor\AppData\Local\pip\Cache' - Restored
Running Install scripts
choco install openssh -confirm
Chocolatey v0.10.7
Installing the following packages:
openssh
By installing you accept licenses for the packages.
Progress: Downloading openssh 0.0.18.0... 45%openssh not installed. An error occurred during installation:
 The operation has timed out.
openssh package files install completed. Performing other installation steps.
The install of openssh was NOT successful.
openssh not installed. An error occurred during installation:
 The operation has timed out.
Chocolatey installed 0/1 packages. 1 packages failed.
 See the log for details (C:\ProgramData\chocolatey\logs\chocolatey.log).
Failures
 - openssh (exited 1) - openssh not installed. An error occurred during installation:
 The operation has timed out.
Command exited with code 1
Contributor

the0id commented Aug 22, 2017

@rodrigc Thanks, I missed that message.

the0id added some commits Aug 24, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment