Merge lp:~cjwatson/lazr.sshserver/more-key-types into lp:lazr.sshserver

Proposed by Colin Watson
Status: Merged
Merged at revision: 78
Proposed branch: lp:~cjwatson/lazr.sshserver/more-key-types
Merge into: lp:lazr.sshserver
Diff against target: 187 lines (+61/-17)
3 files modified
src/lazr/sshserver/NEWS.txt (+8/-0)
src/lazr/sshserver/auth.py (+4/-0)
src/lazr/sshserver/tests/test_auth.py (+49/-17)
To merge this branch: bzr merge lp:~cjwatson/lazr.sshserver/more-key-types
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+348608@code.launchpad.net

Commit message

Add support-in-principle for authenticating using ECDSA or Ed25519 keys.

Description of the change

This also requires a sufficient version of Twisted (which at the time of writing supports ECDSA but not Ed25519) and support in the Launchpad authserver.

I've confirmed this to work locally with a modified version of Launchpad. Before pushing the Launchpad changes, we need to land these lazr.sshserver changes and upgrade its various users, since otherwise it would be rather confusing that you can add ECDSA keys but not authenticate using them.

Adding Ed25519 here is anticipatory, but it's harmless and will save us a step once Twisted gains Ed25519 support.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/lazr/sshserver/NEWS.txt'
--- src/lazr/sshserver/NEWS.txt 2018-02-26 11:57:53 +0000
+++ src/lazr/sshserver/NEWS.txt 2018-06-27 13:56:12 +0000
@@ -2,6 +2,14 @@
2NEWS for lazr.sshserver2NEWS for lazr.sshserver
3=======================3=======================
44
50.1.8
6=====
7
8- Add support-in-principle for authenticating using ECDSA or Ed25519 keys;
9 this also requires a sufficient version of Twisted (which at the time of
10 writing supports ECDSA but not Ed25519) and support in the Launchpad
11 authserver.
12
50.1.7 (2018-02-26)130.1.7 (2018-02-26)
6==================14==================
715
816
=== modified file 'src/lazr/sshserver/auth.py'
--- src/lazr/sshserver/auth.py 2018-02-26 11:49:59 +0000
+++ src/lazr/sshserver/auth.py 2018-06-27 13:56:12 +0000
@@ -314,6 +314,10 @@
314 wantKeyType = 'DSA'314 wantKeyType = 'DSA'
315 elif credentials.algName == 'ssh-rsa':315 elif credentials.algName == 'ssh-rsa':
316 wantKeyType = 'RSA'316 wantKeyType = 'RSA'
317 elif credentials.algName.startswith('ecdsa-sha2-'):
318 wantKeyType = 'ECDSA'
319 elif credentials.algName == 'ssh-ed25519':
320 wantKeyType = 'ED25519'
317 else:321 else:
318 # unknown key type322 # unknown key type
319 return False323 return False
320324
=== modified file 'src/lazr/sshserver/tests/test_auth.py'
--- src/lazr/sshserver/tests/test_auth.py 2018-02-26 11:49:59 +0000
+++ src/lazr/sshserver/tests/test_auth.py 2018-06-27 13:56:12 +0000
@@ -416,7 +416,10 @@
416416
417 valid_user = 'valid_user'417 valid_user = 'valid_user'
418 no_key_user = 'no_key_user'418 no_key_user = 'no_key_user'
419 valid_key = 'valid_key'419 valid_key_rsa = 'valid_key_rsa'
420 valid_key_dsa = 'valid_key_dsa'
421 valid_key_ecdsa = 'valid_key_ecdsa'
422 valid_key_ed25519 = 'valid_key_ed25519'
420423
421 def __init__(self):424 def __init__(self):
422 self.calls = []425 self.calls = []
@@ -430,7 +433,12 @@
430 if username == self.valid_user:433 if username == self.valid_user:
431 return defer.succeed({434 return defer.succeed({
432 'name': username,435 'name': username,
433 'keys': [('DSA', self.valid_key.encode('base64'))],436 'keys': [
437 ('RSA', self.valid_key_rsa.encode('base64')),
438 ('DSA', self.valid_key_dsa.encode('base64')),
439 ('ECDSA', self.valid_key_ecdsa.encode('base64')),
440 ('ED25519', self.valid_key_ed25519.encode('base64')),
441 ],
434 })442 })
435 elif username == self.no_key_user:443 elif username == self.no_key_user:
436 return defer.succeed({444 return defer.succeed({
@@ -443,11 +451,11 @@
443 except auth.NoSuchPersonWithName:451 except auth.NoSuchPersonWithName:
444 return defer.fail()452 return defer.fail()
445453
446 def makeCredentials(self, username, public_key, mind=None):454 def makeCredentials(self, username, key_type, public_key, mind=None):
447 if mind is None:455 if mind is None:
448 mind = auth.UserDetailsMind()456 mind = auth.UserDetailsMind()
449 return auth.SSHPrivateKeyWithMind(457 return auth.SSHPrivateKeyWithMind(
450 username, 'ssh-dss', public_key, '', None, mind)458 username, key_type, public_key, '', None, mind)
451459
452 def makeChecker(self, do_signature_checking=False):460 def makeChecker(self, do_signature_checking=False):
453 """Construct a PublicKeyFromLaunchpadChecker.461 """Construct a PublicKeyFromLaunchpadChecker.
@@ -469,21 +477,29 @@
469 super(TestPublicKeyFromLaunchpadChecker, self).setUp()477 super(TestPublicKeyFromLaunchpadChecker, self).setUp()
470 self.authserver = self.FakeAuthenticationEndpoint()478 self.authserver = self.FakeAuthenticationEndpoint()
471479
480 @defer.inlineCallbacks
472 def test_successful(self):481 def test_successful(self):
473 # Attempting to log in with a username and key known to the482 # Attempting to log in with a username and key known to the
474 # authentication end-point succeeds.483 # authentication end-point succeeds.
475 creds = self.makeCredentials(484 for key_type, public_key in (
476 self.authserver.valid_user, self.authserver.valid_key)485 ('ssh-rsa', self.authserver.valid_key_rsa),
477 checker = self.makeChecker()486 ('ssh-dss', self.authserver.valid_key_dsa),
478 d = checker.requestAvatarId(creds)487 ('ecdsa-sha2-nistp256', self.authserver.valid_key_ecdsa),
479 return d.addCallback(self.assertEqual, self.authserver.valid_user)488 ('ssh-ed25519', self.authserver.valid_key_ed25519),
489 ):
490 creds = self.makeCredentials(
491 self.authserver.valid_user, key_type, public_key)
492 checker = self.makeChecker()
493 username = yield checker.requestAvatarId(creds)
494 self.assertEqual(self.authserver.valid_user, username)
480495
481 @suppress_stderr496 @suppress_stderr
482 def test_invalid_signature(self):497 def test_invalid_signature(self):
483 # The checker requests attempts to authenticate if the requests have498 # The checker requests attempts to authenticate if the requests have
484 # an invalid signature.499 # an invalid signature.
485 creds = self.makeCredentials(500 creds = self.makeCredentials(
486 self.authserver.valid_user, self.authserver.valid_key)501 self.authserver.valid_user, 'ssh-dss',
502 self.authserver.valid_key_dsa)
487 creds.signature = 'a'503 creds.signature = 'a'
488 checker = self.makeChecker(True)504 checker = self.makeChecker(True)
489 d = checker.requestAvatarId(creds)505 d = checker.requestAvatarId(creds)
@@ -519,7 +535,7 @@
519 # Launchpad user names is public.535 # Launchpad user names is public.
520 checker = self.makeChecker()536 checker = self.makeChecker()
521 creds = self.makeCredentials(537 creds = self.makeCredentials(
522 'no-such-user', self.authserver.valid_key)538 'no-such-user', 'ssh-dss', self.authserver.valid_key_dsa)
523 return self.assertLoginError(539 return self.assertLoginError(
524 checker, creds, 'No such Launchpad account: no-such-user')540 checker, creds, 'No such Launchpad account: no-such-user')
525541
@@ -528,7 +544,8 @@
528 # server informs you that the account has no keys.544 # server informs you that the account has no keys.
529 checker = self.makeChecker()545 checker = self.makeChecker()
530 creds = self.makeCredentials(546 creds = self.makeCredentials(
531 self.authserver.no_key_user, self.authserver.valid_key)547 self.authserver.no_key_user, 'ssh-dss',
548 self.authserver.valid_key_dsa)
532 return self.assertLoginError(549 return self.assertLoginError(
533 checker, creds,550 checker, creds,
534 "Launchpad user %r doesn't have a registered SSH key"551 "Launchpad user %r doesn't have a registered SSH key"
@@ -540,7 +557,7 @@
540 # tries several keys as part of normal operation.557 # tries several keys as part of normal operation.
541 checker = self.makeChecker()558 checker = self.makeChecker()
542 creds = self.makeCredentials(559 creds = self.makeCredentials(
543 self.authserver.valid_user, 'invalid key')560 self.authserver.valid_user, 'ssh-dss', 'invalid key')
544 # We cannot use assertLoginError because we are checking that we fail561 # We cannot use assertLoginError because we are checking that we fail
545 # with UnauthorizedLogin and not its subclass562 # with UnauthorizedLogin and not its subclass
546 # UserDisplayedUnauthorizedLogin.563 # UserDisplayedUnauthorizedLogin.
@@ -555,6 +572,20 @@
555 return d572 return d
556573
557 @defer.inlineCallbacks574 @defer.inlineCallbacks
575 def test_unknownKeyType(self):
576 # Authenticating using a key with an unknown type fails.
577 creds = self.makeCredentials(
578 self.authserver.valid_user, 'nonsense',
579 self.authserver.valid_key_rsa)
580 checker = self.makeChecker()
581 exception = yield assert_fails_with(
582 checker.requestAvatarId(creds),
583 UnauthorizedLogin)
584 self.assertFalse(
585 isinstance(exception, auth.UserDisplayedUnauthorizedLogin),
586 "Should not be a UserDisplayedUnauthorizedLogin")
587
588 @defer.inlineCallbacks
558 def test_successful_with_second_key_calls_authserver_once(self):589 def test_successful_with_second_key_calls_authserver_once(self):
559 # It is normal in SSH authentication to be presented with a number of590 # It is normal in SSH authentication to be presented with a number of
560 # keys. When the valid key is presented after some invalid ones (a)591 # keys. When the valid key is presented after some invalid ones (a)
@@ -563,9 +594,10 @@
563 checker = self.makeChecker()594 checker = self.makeChecker()
564 mind = auth.UserDetailsMind()595 mind = auth.UserDetailsMind()
565 wrong_key_creds = self.makeCredentials(596 wrong_key_creds = self.makeCredentials(
566 self.authserver.valid_user, 'invalid key', mind)597 self.authserver.valid_user, 'ssh-dss', 'invalid key', mind)
567 right_key_creds = self.makeCredentials(598 right_key_creds = self.makeCredentials(
568 self.authserver.valid_user, self.authserver.valid_key, mind)599 self.authserver.valid_user, 'ssh-dss',
600 self.authserver.valid_key_dsa, mind)
569 try:601 try:
570 username = yield checker.requestAvatarId(wrong_key_creds)602 username = yield checker.requestAvatarId(wrong_key_creds)
571 except UnauthorizedLogin:603 except UnauthorizedLogin:
@@ -579,9 +611,9 @@
579 checker = self.makeChecker()611 checker = self.makeChecker()
580 mind = auth.UserDetailsMind()612 mind = auth.UserDetailsMind()
581 creds_1 = self.makeCredentials(613 creds_1 = self.makeCredentials(
582 'invalid-user', 'invalid key 1', mind)614 'invalid-user', 'ssh-dss', 'invalid key 1', mind)
583 creds_2 = self.makeCredentials(615 creds_2 = self.makeCredentials(
584 'invalid-user', 'invalid key 2', mind)616 'invalid-user', 'ssh-dss', 'invalid key 2', mind)
585 d = checker.requestAvatarId(creds_1)617 d = checker.requestAvatarId(creds_1)
586618
587 def try_second_key(failure):619 def try_second_key(failure):

Subscribers

People subscribed via source and target branches

to status/vote changes: