We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Support for SSH Token based on ed25519-sk or ecdsa-sk
**Is your feature request related to a problem? Please describe.**
What are you trying to achieve?

I try to add a ssh public key token  based on ed25519-sk (associated with a Yubikey)

[2021-06-21 11:46:36,715][1177539][139641674618752][ERROR][privacyidea.app:1891] Exception on /token/init [POST]
Traceback (most recent call last):
  File "/opt/privacyidea/lib/python3.8/site-packages/flask/app.py", line 2447, in wsgi_app
    response = self.full_dispatch_request()
  File "/opt/privacyidea/lib/python3.8/site-packages/flask/app.py", line 1952, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/opt/privacyidea/lib/python3.8/site-packages/flask/app.py", line 1821, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/opt/privacyidea/lib/python3.8/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/opt/privacyidea/lib/python3.8/site-packages/flask/app.py", line 1950, in full_dispatch_request
    rv = self.dispatch_request()
  File "/opt/privacyidea/lib/python3.8/site-packages/flask/app.py", line 1936, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/opt/privacyidea/lib/python3.8/site-packages/privacyidea/api/lib/prepolicy.py", line 154, in policy_wrapper
    return wrapped_function(*args, **kwds)
  File "/opt/privacyidea/lib/python3.8/site-packages/privacyidea/api/lib/prepolicy.py", line 154, in policy_wrapper
    return wrapped_function(*args, **kwds)
  File "/opt/privacyidea/lib/python3.8/site-packages/privacyidea/api/lib/prepolicy.py", line 154, in policy_wrapper
    return wrapped_function(*args, **kwds)
  [Previous line repeated 21 more times]
  File "/opt/privacyidea/lib/python3.8/site-packages/privacyidea/api/lib/postpolicy.py", line 108, in policy_wrapper
    response = wrapped_function(*args, **kwds)
  File "/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/subscriptions.py", line 333, in check_subscription_wrapper
    f_result = func(*args, **kwds)
  File "/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/event.py", line 99, in event_wrapper
    f_result = func(*args, **kwds)
  File "/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/log.py", line 155, in log_wrapper
    return func(*args, **kwds)
  File "/opt/privacyidea/lib/python3.8/site-packages/privacyidea/api/token.py", line 270, in init
    tokenobject = init_token(param,
  File "/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/log.py", line 155, in log_wrapper
    return func(*args, **kwds)
  File "/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/token.py", line 1085, in init_token
    tokenobject.update(upd_params)
  File "/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/tokens/sshkeytoken.py", line 131, in update
    raise Exception("The keytype you specified is not supported.")
Exception: The keytype you specified is not supported.

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

I want to register this kind of keytype

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

Using gpg slot and generate rsa pubkey

**Additional context**
Add any other context or screenshots, that might help us to better understand your idea, your need and your circumstances.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `privacyidea/lib/tokens/sshkeytoken.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 #
3 #  privacyIDEA
4 #  Jul 18, 2014 Cornelius Kölbel
5 #  License:  AGPLv3
6 #  contact:  http://www.privacyidea.org
7 #
8 # This code is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
10 # License as published by the Free Software Foundation; either
11 # version 3 of the License, or any later version.
12 #
13 # This code is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU AFFERO GENERAL PUBLIC LICENSE for more details.
17 #
18 # You should have received a copy of the GNU Affero General Public
19 # License along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 __doc__="""The SSHKeyTokenClass provides a TokenClass that stores the public
22 SSH key. This can be used to manage SSH keys and retrieve the public ssh key
23 to import it to authorized keys files.
24 
25 The code is tested in tests/test_lib_tokens_ssh
26 """
27 
28 import logging
29 from privacyidea.lib import _
30 from privacyidea.api.lib.utils import getParam
31 from privacyidea.lib.log import log_with
32 from privacyidea.lib.tokenclass import TokenClass, ROLLOUTSTATE
33 from privacyidea.lib.policy import SCOPE, ACTION, GROUP
34 
35 log = logging.getLogger(__name__)
36 
37 
38 optional = True
39 required = False
40 
41 
42 ##TODO: We should save a fingerprint of the SSH Key in the encrypted OTP
43 # field, so that we can be sure, that the public ssh key was not changed in
44 # the database!
45 
46 
47 class SSHkeyTokenClass(TokenClass):
48     """
49     The SSHKeyTokenClass provides a TokenClass that stores the public
50     SSH key. This can be used to manage SSH keys and retrieve the public ssh key
51     to import it to authorized keys files.
52     """
53     mode = ['authenticate']
54     using_pin = False
55 
56     def __init__(self, db_token):
57         TokenClass.__init__(self, db_token)
58         self.set_type(u"sshkey")
59 
60     @staticmethod
61     def get_class_type():
62         return "sshkey"
63 
64     @staticmethod
65     def get_class_prefix():
66         return "SSHK"
67 
68     @staticmethod
69     @log_with(log)
70     def get_class_info(key=None, ret='all'):
71         """
72         returns a subtree of the token definition
73 
74         :param key: subsection identifier
75         :type key: string
76         :param ret: default return value, if nothing is found
77         :type ret: user defined
78         :return: subsection if key exists or user defined
79         :rtype: dictionary
80         """
81         res = {'type': 'sshkey',
82                'title': 'SSHkey Token',
83                'description': _('SSH Public Key: The public SSH key.'),
84                'config': {},
85                'user': ['enroll'],
86                # This tokentype is enrollable in the UI for...
87                'ui_enroll': ["admin", "user"],
88                'policy': {
89                    SCOPE.ENROLL: {
90                        ACTION.MAXTOKENUSER: {
91                            'type': 'int',
92                            'desc': _("The user may only have this maximum number of SSH keys assigned."),
93                            'group': GROUP.TOKEN
94                        },
95                        ACTION.MAXACTIVETOKENUSER: {
96                            'type': 'int',
97                            'desc': _(
98                                "The user may only have this maximum number of active SSH keys assigned."),
99                            'group': GROUP.TOKEN
100                        }
101                    }
102                },
103                }
104         if key:
105             ret = res.get(key, {})
106         else:
107             if ret == 'all':
108                 ret = res
109 
110         return ret
111 
112     def update(self, param):
113         """
114         The key holds the public ssh key and this is required
115         
116         The key probably is of the form "ssh-rsa BASE64 comment"
117         """
118         # We need to save the token, so that we can later add the tokeninfo
119         # Otherwise we might not have created the DB entry, yet and we would
120         # be missing the token.id
121         self.token.save()
122 
123         getParam(param, "sshkey", required)
124             
125         key_elem = param.get("sshkey").split(" ", 2)
126         if key_elem[0] not in ["ssh-rsa", "ssh-ed25519", "ecdsa-sha2-nistp256"]:
127             self.token.rollout_state = ROLLOUTSTATE.BROKEN
128             self.token.save()
129             raise Exception("The keytype you specified is not supported.")
130 
131         if len(key_elem) < 2:
132             self.token.rollout_state = ROLLOUTSTATE.BROKEN
133             self.token.save()
134             raise Exception("Missing key.")
135 
136         key_type = key_elem[0]
137         key = key_elem[1]
138         if len(key_elem) > 2:
139             key_comment = key_elem[2]
140         else:
141             key_comment = ""
142         
143         # convert key to hex
144         self.add_tokeninfo("ssh_key", key, value_type="password")
145         self.add_tokeninfo("ssh_type", key_type)
146         self.add_tokeninfo("ssh_comment", key_comment)
147 
148         # call the parents function
149         TokenClass.update(self, param)
150         
151     @log_with(log)
152     def get_sshkey(self):
153         """
154         returns the public SSH key
155         
156         :return: SSH pub key
157         :rtype: string
158         """
159         ti = self.get_tokeninfo()
160         key_type = ti.get("ssh_type")
161         key_comment = ti.get("ssh_comment")
162         # get the ssh key directly, otherwise it will not be decrypted
163         sshkey = self.get_tokeninfo("ssh_key")
164         return u"{0!s} {1!s} {2!s}".format(key_type, sshkey, key_comment)
165
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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
@@ -28,6 +28,7 @@
 import logging
 from privacyidea.lib import _
 from privacyidea.api.lib.utils import getParam
+from privacyidea.lib.error import TokenAdminError
 from privacyidea.lib.log import log_with
 from privacyidea.lib.tokenclass import TokenClass, ROLLOUTSTATE
 from privacyidea.lib.policy import SCOPE, ACTION, GROUP
@@ -121,17 +122,18 @@
         self.token.save()
 
         getParam(param, "sshkey", required)
-            
+
         key_elem = param.get("sshkey").split(" ", 2)
-        if key_elem[0] not in ["ssh-rsa", "ssh-ed25519", "ecdsa-sha2-nistp256"]:
+        if key_elem[0] not in ["ssh-rsa", "ssh-ed25519", "ecdsa-sha2-nistp256",
+                               "sk-ecdsa-sha2-nistp256@openssh.com", "sk-ssh-ed25519@openssh.com"]:
             self.token.rollout_state = ROLLOUTSTATE.BROKEN
             self.token.save()
-            raise Exception("The keytype you specified is not supported.")
+            raise TokenAdminError("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.")
+            raise TokenAdminError("Missing key.")
 
         key_type = key_elem[0]
         key = key_elem[1]
@@ -161,4 +163,7 @@
         key_comment = ti.get("ssh_comment")
         # get the ssh key directly, otherwise it will not be decrypted
         sshkey = self.get_tokeninfo("ssh_key")
-        return u"{0!s} {1!s} {2!s}".format(key_type, sshkey, key_comment)
+        r = u"{0!s} {1!s}".format(key_type, sshkey)
+        if key_comment:
+            r += " " + key_comment
+        return r

{"golden_diff": "diff --git a/privacyidea/lib/tokens/sshkeytoken.py b/privacyidea/lib/tokens/sshkeytoken.py\n--- a/privacyidea/lib/tokens/sshkeytoken.py\n+++ b/privacyidea/lib/tokens/sshkeytoken.py\n@@ -28,6 +28,7 @@\n import logging\n from privacyidea.lib import _\n from privacyidea.api.lib.utils import getParam\n+from privacyidea.lib.error import TokenAdminError\n from privacyidea.lib.log import log_with\n from privacyidea.lib.tokenclass import TokenClass, ROLLOUTSTATE\n from privacyidea.lib.policy import SCOPE, ACTION, GROUP\n@@ -121,17 +122,18 @@\n         self.token.save()\n \n         getParam(param, \"sshkey\", required)\n-            \n+\n         key_elem = param.get(\"sshkey\").split(\" \", 2)\n-        if key_elem[0] not in [\"ssh-rsa\", \"ssh-ed25519\", \"ecdsa-sha2-nistp256\"]:\n+        if key_elem[0] not in [\"ssh-rsa\", \"ssh-ed25519\", \"ecdsa-sha2-nistp256\",\n+                               \"sk-ecdsa-sha2-nistp256@openssh.com\", \"sk-ssh-ed25519@openssh.com\"]:\n             self.token.rollout_state = ROLLOUTSTATE.BROKEN\n             self.token.save()\n-            raise Exception(\"The keytype you specified is not supported.\")\n+            raise TokenAdminError(\"The keytype you specified is not supported.\")\n \n         if len(key_elem) < 2:\n             self.token.rollout_state = ROLLOUTSTATE.BROKEN\n             self.token.save()\n-            raise Exception(\"Missing key.\")\n+            raise TokenAdminError(\"Missing key.\")\n \n         key_type = key_elem[0]\n         key = key_elem[1]\n@@ -161,4 +163,7 @@\n         key_comment = ti.get(\"ssh_comment\")\n         # get the ssh key directly, otherwise it will not be decrypted\n         sshkey = self.get_tokeninfo(\"ssh_key\")\n-        return u\"{0!s} {1!s} {2!s}\".format(key_type, sshkey, key_comment)\n+        r = u\"{0!s} {1!s}\".format(key_type, sshkey)\n+        if key_comment:\n+            r += \" \" + key_comment\n+        return r\n", "issue": "Support for SSH Token based on ed25519-sk or ecdsa-sk\n**Is your feature request related to a problem? Please describe.**\r\nWhat are you trying to achieve?\r\n\r\nI try to add a ssh public key token  based on ed25519-sk (associated with a Yubikey)\r\n\r\n[2021-06-21 11:46:36,715][1177539][139641674618752][ERROR][privacyidea.app:1891] Exception on /token/init [POST]\r\nTraceback (most recent call last):\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/flask/app.py\", line 2447, in wsgi_app\r\n    response = self.full_dispatch_request()\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/flask/app.py\", line 1952, in full_dispatch_request\r\n    rv = self.handle_user_exception(e)\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/flask/app.py\", line 1821, in handle_user_exception\r\n    reraise(exc_type, exc_value, tb)\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/flask/_compat.py\", line 39, in reraise\r\n    raise value\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/flask/app.py\", line 1950, in full_dispatch_request\r\n    rv = self.dispatch_request()\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/flask/app.py\", line 1936, in dispatch_request\r\n    return self.view_functions[rule.endpoint](**req.view_args)\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/privacyidea/api/lib/prepolicy.py\", line 154, in policy_wrapper\r\n    return wrapped_function(*args, **kwds)\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/privacyidea/api/lib/prepolicy.py\", line 154, in policy_wrapper\r\n    return wrapped_function(*args, **kwds)\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/privacyidea/api/lib/prepolicy.py\", line 154, in policy_wrapper\r\n    return wrapped_function(*args, **kwds)\r\n  [Previous line repeated 21 more times]\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/privacyidea/api/lib/postpolicy.py\", line 108, in policy_wrapper\r\n    response = wrapped_function(*args, **kwds)\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/subscriptions.py\", line 333, in check_subscription_wrapper\r\n    f_result = func(*args, **kwds)\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/event.py\", line 99, in event_wrapper\r\n    f_result = func(*args, **kwds)\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/log.py\", line 155, in log_wrapper\r\n    return func(*args, **kwds)\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/privacyidea/api/token.py\", line 270, in init\r\n    tokenobject = init_token(param,\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/log.py\", line 155, in log_wrapper\r\n    return func(*args, **kwds)\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/token.py\", line 1085, in init_token\r\n    tokenobject.update(upd_params)\r\n  File \"/opt/privacyidea/lib/python3.8/site-packages/privacyidea/lib/tokens/sshkeytoken.py\", line 131, in update\r\n    raise Exception(\"The keytype you specified is not supported.\")\r\nException: The keytype you specified is not supported.\r\n\r\n**Describe the solution you'd like**\r\nA clear and concise description of what you want to happen.\r\n\r\nI want to register this kind of keytype\r\n\r\n**Describe alternatives you've considered**\r\nA clear and concise description of any alternative solutions or features you've considered.\r\n\r\nUsing gpg slot and generate rsa pubkey\r\n\r\n**Additional context**\r\nAdd any other context or screenshots, that might help us to better understand your idea, your need and your circumstances.\r\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n#\n#  privacyIDEA\n#  Jul 18, 2014 Cornelius K\u00f6lbel\n#  License:  AGPLv3\n#  contact:  http://www.privacyidea.org\n#\n# This code is free software; you can redistribute it and/or\n# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE\n# License as published by the Free Software Foundation; either\n# version 3 of the License, or any later version.\n#\n# This code is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU AFFERO GENERAL PUBLIC LICENSE for more details.\n#\n# You should have received a copy of the GNU Affero General Public\n# License along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#\n__doc__=\"\"\"The SSHKeyTokenClass provides a TokenClass that stores the public\nSSH key. This can be used to manage SSH keys and retrieve the public ssh key\nto import it to authorized keys files.\n\nThe code is tested in tests/test_lib_tokens_ssh\n\"\"\"\n\nimport logging\nfrom privacyidea.lib import _\nfrom privacyidea.api.lib.utils import getParam\nfrom privacyidea.lib.log import log_with\nfrom privacyidea.lib.tokenclass import TokenClass, ROLLOUTSTATE\nfrom privacyidea.lib.policy import SCOPE, ACTION, GROUP\n\nlog = logging.getLogger(__name__)\n\n\noptional = True\nrequired = False\n\n\n##TODO: We should save a fingerprint of the SSH Key in the encrypted OTP\n# field, so that we can be sure, that the public ssh key was not changed in\n# the database!\n\n\nclass SSHkeyTokenClass(TokenClass):\n    \"\"\"\n    The SSHKeyTokenClass provides a TokenClass that stores the public\n    SSH key. This can be used to manage SSH keys and retrieve the public ssh key\n    to import it to authorized keys files.\n    \"\"\"\n    mode = ['authenticate']\n    using_pin = False\n\n    def __init__(self, db_token):\n        TokenClass.__init__(self, db_token)\n        self.set_type(u\"sshkey\")\n\n    @staticmethod\n    def get_class_type():\n        return \"sshkey\"\n\n    @staticmethod\n    def get_class_prefix():\n        return \"SSHK\"\n\n    @staticmethod\n    @log_with(log)\n    def get_class_info(key=None, ret='all'):\n        \"\"\"\n        returns a subtree of the token definition\n\n        :param key: subsection identifier\n        :type key: string\n        :param ret: default return value, if nothing is found\n        :type ret: user defined\n        :return: subsection if key exists or user defined\n        :rtype: dictionary\n        \"\"\"\n        res = {'type': 'sshkey',\n               'title': 'SSHkey Token',\n               'description': _('SSH Public Key: The public SSH key.'),\n               'config': {},\n               'user': ['enroll'],\n               # This tokentype is enrollable in the UI for...\n               'ui_enroll': [\"admin\", \"user\"],\n               'policy': {\n                   SCOPE.ENROLL: {\n                       ACTION.MAXTOKENUSER: {\n                           'type': 'int',\n                           'desc': _(\"The user may only have this maximum number of SSH keys assigned.\"),\n                           'group': GROUP.TOKEN\n                       },\n                       ACTION.MAXACTIVETOKENUSER: {\n                           'type': 'int',\n                           'desc': _(\n                               \"The user may only have this maximum number of active SSH keys assigned.\"),\n                           'group': GROUP.TOKEN\n                       }\n                   }\n               },\n               }\n        if key:\n            ret = res.get(key, {})\n        else:\n            if ret == 'all':\n                ret = res\n\n        return ret\n\n    def update(self, param):\n        \"\"\"\n        The key holds the public ssh key and this is required\n        \n        The key probably is of the form \"ssh-rsa BASE64 comment\"\n        \"\"\"\n        # We need to save the token, so that we can later add the tokeninfo\n        # Otherwise we might not have created the DB entry, yet and we would\n        # be missing the token.id\n        self.token.save()\n\n        getParam(param, \"sshkey\", required)\n            \n        key_elem = param.get(\"sshkey\").split(\" \", 2)\n        if key_elem[0] not in [\"ssh-rsa\", \"ssh-ed25519\", \"ecdsa-sha2-nistp256\"]:\n            self.token.rollout_state = ROLLOUTSTATE.BROKEN\n            self.token.save()\n            raise Exception(\"The keytype you specified is not supported.\")\n\n        if len(key_elem) < 2:\n            self.token.rollout_state = ROLLOUTSTATE.BROKEN\n            self.token.save()\n            raise Exception(\"Missing key.\")\n\n        key_type = key_elem[0]\n        key = key_elem[1]\n        if len(key_elem) > 2:\n            key_comment = key_elem[2]\n        else:\n            key_comment = \"\"\n        \n        # convert key to hex\n        self.add_tokeninfo(\"ssh_key\", key, value_type=\"password\")\n        self.add_tokeninfo(\"ssh_type\", key_type)\n        self.add_tokeninfo(\"ssh_comment\", key_comment)\n\n        # call the parents function\n        TokenClass.update(self, param)\n        \n    @log_with(log)\n    def get_sshkey(self):\n        \"\"\"\n        returns the public SSH key\n        \n        :return: SSH pub key\n        :rtype: string\n        \"\"\"\n        ti = self.get_tokeninfo()\n        key_type = ti.get(\"ssh_type\")\n        key_comment = ti.get(\"ssh_comment\")\n        # get the ssh key directly, otherwise it will not be decrypted\n        sshkey = self.get_tokeninfo(\"ssh_key\")\n        return u\"{0!s} {1!s} {2!s}\".format(key_type, sshkey, key_comment)\n", "path": "privacyidea/lib/tokens/sshkeytoken.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n#\n#  privacyIDEA\n#  Jul 18, 2014 Cornelius K\u00f6lbel\n#  License:  AGPLv3\n#  contact:  http://www.privacyidea.org\n#\n# This code is free software; you can redistribute it and/or\n# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE\n# License as published by the Free Software Foundation; either\n# version 3 of the License, or any later version.\n#\n# This code is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU AFFERO GENERAL PUBLIC LICENSE for more details.\n#\n# You should have received a copy of the GNU Affero General Public\n# License along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#\n__doc__=\"\"\"The SSHKeyTokenClass provides a TokenClass that stores the public\nSSH key. This can be used to manage SSH keys and retrieve the public ssh key\nto import it to authorized keys files.\n\nThe code is tested in tests/test_lib_tokens_ssh\n\"\"\"\n\nimport logging\nfrom privacyidea.lib import _\nfrom privacyidea.api.lib.utils import getParam\nfrom privacyidea.lib.error import TokenAdminError\nfrom privacyidea.lib.log import log_with\nfrom privacyidea.lib.tokenclass import TokenClass, ROLLOUTSTATE\nfrom privacyidea.lib.policy import SCOPE, ACTION, GROUP\n\nlog = logging.getLogger(__name__)\n\n\noptional = True\nrequired = False\n\n\n##TODO: We should save a fingerprint of the SSH Key in the encrypted OTP\n# field, so that we can be sure, that the public ssh key was not changed in\n# the database!\n\n\nclass SSHkeyTokenClass(TokenClass):\n    \"\"\"\n    The SSHKeyTokenClass provides a TokenClass that stores the public\n    SSH key. This can be used to manage SSH keys and retrieve the public ssh key\n    to import it to authorized keys files.\n    \"\"\"\n    mode = ['authenticate']\n    using_pin = False\n\n    def __init__(self, db_token):\n        TokenClass.__init__(self, db_token)\n        self.set_type(u\"sshkey\")\n\n    @staticmethod\n    def get_class_type():\n        return \"sshkey\"\n\n    @staticmethod\n    def get_class_prefix():\n        return \"SSHK\"\n\n    @staticmethod\n    @log_with(log)\n    def get_class_info(key=None, ret='all'):\n        \"\"\"\n        returns a subtree of the token definition\n\n        :param key: subsection identifier\n        :type key: string\n        :param ret: default return value, if nothing is found\n        :type ret: user defined\n        :return: subsection if key exists or user defined\n        :rtype: dictionary\n        \"\"\"\n        res = {'type': 'sshkey',\n               'title': 'SSHkey Token',\n               'description': _('SSH Public Key: The public SSH key.'),\n               'config': {},\n               'user': ['enroll'],\n               # This tokentype is enrollable in the UI for...\n               'ui_enroll': [\"admin\", \"user\"],\n               'policy': {\n                   SCOPE.ENROLL: {\n                       ACTION.MAXTOKENUSER: {\n                           'type': 'int',\n                           'desc': _(\"The user may only have this maximum number of SSH keys assigned.\"),\n                           'group': GROUP.TOKEN\n                       },\n                       ACTION.MAXACTIVETOKENUSER: {\n                           'type': 'int',\n                           'desc': _(\n                               \"The user may only have this maximum number of active SSH keys assigned.\"),\n                           'group': GROUP.TOKEN\n                       }\n                   }\n               },\n               }\n        if key:\n            ret = res.get(key, {})\n        else:\n            if ret == 'all':\n                ret = res\n\n        return ret\n\n    def update(self, param):\n        \"\"\"\n        The key holds the public ssh key and this is required\n        \n        The key probably is of the form \"ssh-rsa BASE64 comment\"\n        \"\"\"\n        # We need to save the token, so that we can later add the tokeninfo\n        # Otherwise we might not have created the DB entry, yet and we would\n        # be missing the token.id\n        self.token.save()\n\n        getParam(param, \"sshkey\", required)\n\n        key_elem = param.get(\"sshkey\").split(\" \", 2)\n        if key_elem[0] not in [\"ssh-rsa\", \"ssh-ed25519\", \"ecdsa-sha2-nistp256\",\n                               \"sk-ecdsa-sha2-nistp256@openssh.com\", \"sk-ssh-ed25519@openssh.com\"]:\n            self.token.rollout_state = ROLLOUTSTATE.BROKEN\n            self.token.save()\n            raise TokenAdminError(\"The keytype you specified is not supported.\")\n\n        if len(key_elem) < 2:\n            self.token.rollout_state = ROLLOUTSTATE.BROKEN\n            self.token.save()\n            raise TokenAdminError(\"Missing key.\")\n\n        key_type = key_elem[0]\n        key = key_elem[1]\n        if len(key_elem) > 2:\n            key_comment = key_elem[2]\n        else:\n            key_comment = \"\"\n        \n        # convert key to hex\n        self.add_tokeninfo(\"ssh_key\", key, value_type=\"password\")\n        self.add_tokeninfo(\"ssh_type\", key_type)\n        self.add_tokeninfo(\"ssh_comment\", key_comment)\n\n        # call the parents function\n        TokenClass.update(self, param)\n        \n    @log_with(log)\n    def get_sshkey(self):\n        \"\"\"\n        returns the public SSH key\n        \n        :return: SSH pub key\n        :rtype: string\n        \"\"\"\n        ti = self.get_tokeninfo()\n        key_type = ti.get(\"ssh_type\")\n        key_comment = ti.get(\"ssh_comment\")\n        # get the ssh key directly, otherwise it will not be decrypted\n        sshkey = self.get_tokeninfo(\"ssh_key\")\n        r = u\"{0!s} {1!s}\".format(key_type, sshkey)\n        if key_comment:\n            r += \" \" + key_comment\n        return r\n", "path": "privacyidea/lib/tokens/sshkeytoken.py"}]}