Repository: privacyidea/privacyidea

Issue:
Support other keytypes with SSHKey Token
We would like to import other public key types like ecdsa into the SSHKey token type.

* [x] The UI and the /token/init API needs to allow this.
* [x] We also need to check, if the keytype is saved and returned correctly by GET /authitem



Discussion:

Resolving patch:
diff --git a/privacyidea/lib/tokens/sshkeytoken.py b/privacyidea/lib/tokens/sshkeytoken.py
--- a/privacyidea/lib/tokens/sshkeytoken.py
+++ b/privacyidea/lib/tokens/sshkeytoken.py
@@ -125,8 +125,10 @@ def update(self, param):
         getParam(param, "sshkey", required)
             
         key_elem = param.get("sshkey").split(" ", 2)
-        if len(key_elem) != 3 or key_elem[0] != "ssh-rsa":
-            raise Exception("The key must consist of 'ssh-rsa BASE64 comment'")
+        if len(key_elem) != 3:
+            raise Exception("The key must consist of 'ssh-keytype BASE64 comment'")
+        if key_elem[0] not in ["ssh-rsa", "ssh-ed25519", "ecdsa-sha2-nistp256"]:
+            raise Exception("The keytype you specified is not supported.")
 
         key_type = key_elem[0]
         key = key_elem[1]


Test patch:
diff --git a/tests/test_api_machines.py b/tests/test_api_machines.py
--- a/tests/test_api_machines.py
+++ b/tests/test_api_machines.py
@@ -7,8 +7,8 @@
 from privacyidea.lib.user import User
 from .base import MyApiTestCase
 import json
-from privacyidea.lib.token import init_token, get_tokens
-from privacyidea.lib.machine import attach_token
+from privacyidea.lib.token import init_token, get_tokens, remove_token
+from privacyidea.lib.machine import attach_token, detach_token
 from privacyidea.lib.policy import (set_policy, delete_policy, ACTION, SCOPE)
 
 HOSTSFILE = "tests/testdata/hosts"
@@ -27,6 +27,9 @@
          "afLE9AtAL4nnMPuubC87L0wJ88un9teza/N02KJMHy01Yz3iJKt3Ou9eV6kqO" \
          "ei3kvLs5dXmriTHp6g9whtnN6/Liv9SzZPJTs8YfThi34Wccrw== " \
          "NetKnights GmbH"
+SSHKEY_ecdsa = u"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzd" \
+               u"HAyNTYAAABBBHGCdIk0pO1HFr/mF4oLb43ZRyQJ4K7ICLrAhAiQERVa0tUvyY5TE" \
+               u"zurWTqxSMx203rY77t6xnHLZBMPPpv8rk0= cornelius@puck"
 OTPKEY = "3132333435363738393031323334353637383930"
 
 
@@ -213,7 +216,7 @@ def test_99_detach_token(self):
         token_obj = get_tokens(serial=serial)[0]
         self.assertEqual(len(token_obj.token.machine_list), 0)
 
-    def test_10_auth_items_ssh(self):
+    def test_10_auth_items_ssh_rsa(self):
         # create an SSH token
         token_obj = init_token({"serial": self.serial2, "type": "sshkey",
                                 "sshkey": SSHKEY})
@@ -291,6 +294,48 @@ def test_10_auth_items_ssh(self):
             sshkey = result["value"].get("ssh")[0].get("sshkey")
             self.assertTrue(sshkey.startswith("ssh-rsa"), sshkey)
 
+        detach_token(self.serial2, application="ssh", hostname="gandalf")
+        remove_token(self.serial2)
+
+    def test_10_auth_items_ssh_ecdsa(self):
+        # create an SSH token
+        token_obj = init_token({"serial": self.serial2, "type": "sshkey",
+                                "sshkey": SSHKEY_ecdsa})
+        self.assertEqual(token_obj.type, "sshkey")
+
+        # Attach the token to the machine "gandalf" with the application SSH
+        r = attach_token(hostname="gandalf", serial=self.serial2,
+                         application="ssh", options={"user": "testuser"})
+
+        self.assertEqual(r.machine_id, "192.168.0.1")
+
+        # fetch the auth_items for application SSH on machine gandalf
+        with self.app.test_request_context(
+                '/machine/authitem/ssh?hostname=gandalf',
+                method='GET',
+                headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = res.json.get("result")
+            self.assertEqual(result["status"], True)
+            sshkey = result["value"].get("ssh")[0].get("sshkey")
+            self.assertTrue(sshkey.startswith("ecdsa-sha2-nistp256"), sshkey)
+
+        # fetch the auth_items for user testuser
+        with self.app.test_request_context(
+                '/machine/authitem/ssh?hostname=gandalf&user=testuser',
+                method='GET',
+                headers={'Authorization': self.at}):
+            res = self.app.full_dispatch_request()
+            self.assertTrue(res.status_code == 200, res)
+            result = res.json.get("result")
+            self.assertEqual(result["status"], True)
+            sshkey = result["value"].get("ssh")[0].get("sshkey")
+            self.assertTrue(sshkey.startswith("ecdsa-sha2-nistp256"), sshkey)
+
+        detach_token(self.serial2, application="ssh", hostname="gandalf")
+        remove_token(self.serial2)
+
     def test_11_auth_items_luks(self):
         # create TOTP/Yubikey token
         token_obj = init_token({"serial": self.serial3, "type": "totp",
diff --git a/tests/test_lib_tokens_ssh.py b/tests/test_lib_tokens_ssh.py
--- a/tests/test_lib_tokens_ssh.py
+++ b/tests/test_lib_tokens_ssh.py
@@ -14,6 +14,8 @@ class SSHTokenTestCase(MyTestCase):
 
     otppin = "topsecret"
     serial1 = "ser1"
+    serial2 = "ser2"
+    serial3 = "ser3"
     sshkey = u"ssh-rsa " \
              u"AAAAB3NzaC1yc2EAAAADAQABAAACAQDJy0rLoxqc8SsY8DVAFijMsQyCv" \
              u"hBu4K40hdZOacXK4O6OgnacnSKN56MP6pzz2+4svzvDzwvkFsvf34pbsgD" \
@@ -28,6 +30,12 @@ class SSHTokenTestCase(MyTestCase):
              u"afLE9AtAL4nnMPuubC87L0wJ88un9teza/N02KJMHy01Yz3iJKt3Ou9eV6kqO" \
              u"ei3kvLs5dXmriTHp6g9whtnN6/Liv9SzZPJTs8YfThi34Wccrw== " \
              u"NetKnights GmbH Descröption"
+    unsupported_keytype = "ssh-something AAAAA comment"
+    sshkey_ecdsa = u"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzd" \
+                   u"HAyNTYAAABBBHGCdIk0pO1HFr/mF4oLb43ZRyQJ4K7ICLrAhAiQERVa0tUvyY5TE" \
+                   u"zurWTqxSMx203rY77t6xnHLZBMPPpv8rk0= cornelius@puck"
+    sshkey_ed25519 = u"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC38dIb3tM6nPrT" \
+                     u"3j1UfsQxOCBbf3JogwsKeVPM893Pi cornelius@puck"
     wrong_sshkey = """---- BEGIN SSH2 PUBLIC KEY ----
 AAAAB3NzaC1kc3MAAACBAKrFC6uDvuxl9vnYL/Fu/Vq+12KJF4
 RyMSQe4mn8oHJma2VzepBRBpLt7Q==
@@ -44,6 +52,9 @@ def test_01_create_token(self):
         # An invalid key, raises an exception
         self.assertRaises(Exception, token.update, {"sshkey": self.wrong_sshkey})
 
+        # An unsupported keytype
+        self.assertRaises(Exception, token.update, {"sshkey": self.unsupported_keytype})
+
         # Set valid key
         token.update({"sshkey": self.sshkey})
         self.assertTrue(token.token.serial == self.serial1, token)
@@ -54,6 +65,18 @@ def test_01_create_token(self):
         self.assertTrue(class_prefix == "SSHK", class_prefix)
         self.assertTrue(token.get_class_type() == "sshkey", token)
 
+        # ecdsa
+        db_token = Token(self.serial2, tokentype="sshkey")
+        db_token.save()
+        token = SSHkeyTokenClass(db_token)
+        token.update({"sshkey": self.sshkey_ecdsa})
+
+        # ed25519
+        db_token = Token(self.serial3, tokentype="sshkey")
+        db_token.save()
+        token = SSHkeyTokenClass(db_token)
+        token.update({"sshkey": self.sshkey_ed25519})
+
     def test_03_class_methods(self):
         db_token = Token.query.filter(Token.serial == self.serial1).first()
         token = SSHkeyTokenClass(db_token)
@@ -71,3 +94,15 @@ def test_04_get_sshkey(self):
         sshkey = token.get_sshkey()
         self.assertTrue(sshkey == self.sshkey, sshkey)
         self.assertIsInstance(sshkey, six.text_type)
+
+        db_token = Token.query.filter(Token.serial == self.serial2).first()
+        token = SSHkeyTokenClass(db_token)
+        sshkey = token.get_sshkey()
+        self.assertTrue(sshkey == self.sshkey_ecdsa, sshkey)
+        self.assertIsInstance(sshkey, six.text_type)
+
+        db_token = Token.query.filter(Token.serial == self.serial3).first()
+        token = SSHkeyTokenClass(db_token)
+        sshkey = token.get_sshkey()
+        self.assertTrue(sshkey == self.sshkey_ed25519, sshkey)
+        self.assertIsInstance(sshkey, six.text_type)