Repository: privacyidea/privacyidea

Issue:
Enrolling an ssh public key that does not have the user@host component causes 500
### Top-level intent

Add my ssh key for the form "scheme publickey" that did not contain the "user@host" element. This caused the enrollment to have a server 500 error, but the key was still added (and repeating allows the key to be added multiple times). 

### Steps to reproduce

1. Attempt to add the key as defined

### Expected outcome

Either a proper warning should have been displayed and the key rejected, or the enrollment should have been a success.

### Actual outcome

A 500 error was shown, but the key was also added. Repeating this allows the key to be added multiple times which then requires clean up (so you probably need to also prevent duplicate key addition ...). 




Discussion:
Investigate and solve.

Resolving patch:
diff --git a/privacyidea/lib/tokenclass.py b/privacyidea/lib/tokenclass.py
--- a/privacyidea/lib/tokenclass.py
+++ b/privacyidea/lib/tokenclass.py
@@ -151,6 +151,7 @@ class ROLLOUTSTATE(object):
     # This means the user needs to authenticate to verify that the token was successfully enrolled.
     VERIFYPENDING = 'verify'
     ENROLLED = 'enrolled'
+    BROKEN = 'broken'
 
 
 class TokenClass(object):
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
@@ -29,7 +29,7 @@
 from privacyidea.lib import _
 from privacyidea.api.lib.utils import getParam
 from privacyidea.lib.log import log_with
-from privacyidea.lib.tokenclass import TokenClass
+from privacyidea.lib.tokenclass import TokenClass, ROLLOUTSTATE
 from privacyidea.lib.policy import SCOPE, ACTION, GROUP
 
 log = logging.getLogger(__name__)
@@ -123,14 +123,22 @@ def update(self, param):
         getParam(param, "sshkey", required)
             
         key_elem = param.get("sshkey").split(" ", 2)
-        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"]:
+            self.token.rollout_state = ROLLOUTSTATE.BROKEN
+            self.token.save()
             raise Exception("The keytype you specified is not supported.")
 
+        if len(key_elem) < 2:
+            self.token.rollout_state = ROLLOUTSTATE.BROKEN
+            self.token.save()
+            raise Exception("Missing key.")
+
         key_type = key_elem[0]
         key = key_elem[1]
-        key_comment = key_elem[2]
+        if len(key_elem) > 2:
+            key_comment = key_elem[2]
+        else:
+            key_comment = ""
         
         # convert key to hex
         self.add_tokeninfo("ssh_key", key, value_type="password")


Test patch:
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
@@ -5,6 +5,7 @@
 """
 
 from .base import MyTestCase
+from privacyidea.lib.tokenclass import ROLLOUTSTATE
 from privacyidea.lib.tokens.sshkeytoken import SSHkeyTokenClass
 from privacyidea.models import Token
 import six
@@ -40,6 +41,7 @@ class SSHTokenTestCase(MyTestCase):
 AAAAB3NzaC1kc3MAAACBAKrFC6uDvuxl9vnYL/Fu/Vq+12KJF4
 RyMSQe4mn8oHJma2VzepBRBpLt7Q==
 ---- END SSH2 PUBLIC KEY ----"""
+    INVALID_SSH = "ssh-rsa"
 
     def test_01_create_token(self):
         db_token = Token(self.serial1, tokentype="sshkey")
@@ -48,12 +50,19 @@ def test_01_create_token(self):
 
         # An invalid key, raises an exception
         self.assertRaises(Exception, token.update, {"sshkey": "InvalidKey"})
+        self.assertEqual(token.rollout_state, ROLLOUTSTATE.BROKEN)
+
+        # An invalid key, raises an exception
+        self.assertRaises(Exception, token.update, {"sshkey": self.INVALID_SSH})
+        self.assertEqual(token.rollout_state, ROLLOUTSTATE.BROKEN)
 
         # An invalid key, raises an exception
         self.assertRaises(Exception, token.update, {"sshkey": self.wrong_sshkey})
+        self.assertEqual(token.rollout_state, ROLLOUTSTATE.BROKEN)
 
         # An unsupported keytype
         self.assertRaises(Exception, token.update, {"sshkey": self.unsupported_keytype})
+        self.assertEqual(token.rollout_state, ROLLOUTSTATE.BROKEN)
 
         # Set valid key
         token.update({"sshkey": self.sshkey})
@@ -77,7 +86,7 @@ def test_01_create_token(self):
         token = SSHkeyTokenClass(db_token)
         token.update({"sshkey": self.sshkey_ed25519})
 
-    def test_03_class_methods(self):
+    def test_02_class_methods(self):
         db_token = Token.query.filter(Token.serial == self.serial1).first()
         token = SSHkeyTokenClass(db_token)
 
@@ -88,7 +97,7 @@ def test_03_class_methods(self):
         info = token.get_class_info("title")
         self.assertTrue(info == "SSHkey Token", info)
 
-    def test_04_get_sshkey(self):
+    def test_03_get_sshkey(self):
         db_token = Token.query.filter(Token.serial == self.serial1).first()
         token = SSHkeyTokenClass(db_token)
         sshkey = token.get_sshkey()