Repository: privacyidea/privacyidea

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.



Discussion:
Thank you for filing an issue and sharing your observations or ideas. Please be sure to provide as many information as possible to help us working on this issue.
We should combine this topic with #2712 

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
@@ -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 @@ def update(self, param):
         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 @@ def get_sshkey(self):
         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


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
@@ -3,7 +3,7 @@
 This test file tests the lib.tokens.sshkeytoken
 This depends on lib.tokenclass
 """
-
+from privacyidea.lib.error import TokenAdminError
 from .base import MyTestCase
 from privacyidea.lib.tokenclass import ROLLOUTSTATE
 from privacyidea.lib.tokens.sshkeytoken import SSHkeyTokenClass
@@ -17,6 +17,7 @@ class SSHTokenTestCase(MyTestCase):
     serial1 = "ser1"
     serial2 = "ser2"
     serial3 = "ser3"
+    serial4 = "ser4"
     sshkey = u"ssh-rsa " \
              u"AAAAB3NzaC1yc2EAAAADAQABAAACAQDJy0rLoxqc8SsY8DVAFijMsQyCv" \
              u"hBu4K40hdZOacXK4O6OgnacnSKN56MP6pzz2+4svzvDzwvkFsvf34pbsgD" \
@@ -37,10 +38,14 @@ class SSHTokenTestCase(MyTestCase):
                    u"zurWTqxSMx203rY77t6xnHLZBMPPpv8rk0= cornelius@puck"
     sshkey_ed25519 = u"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC38dIb3tM6nPrT" \
                      u"3j1UfsQxOCBbf3JogwsKeVPM893Pi cornelius@puck"
+    ecdsa_sk = u"sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlz" \
+               u"dHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBOStamg+GO4TSgtoWjc82p" \
+               u"OKZIDuOeAt/8PU/jbzEmth6VuNhghRTCPqPMFtR6mB3Pb12yMDRiLH/t1VwkvWWYIA" \
+               u"AAAEc3NoOg=="
     wrong_sshkey = """---- BEGIN SSH2 PUBLIC KEY ----
-AAAAB3NzaC1kc3MAAACBAKrFC6uDvuxl9vnYL/Fu/Vq+12KJF4
-RyMSQe4mn8oHJma2VzepBRBpLt7Q==
----- END SSH2 PUBLIC KEY ----"""
+    AAAAB3NzaC1kc3MAAACBAKrFC6uDvuxl9vnYL/Fu/Vq+12KJF4
+    RyMSQe4mn8oHJma2VzepBRBpLt7Q==
+    ---- END SSH2 PUBLIC KEY ----"""
     INVALID_SSH = "ssh-rsa"
 
     def test_01_create_token(self):
@@ -49,19 +54,19 @@ def test_01_create_token(self):
         token = SSHkeyTokenClass(db_token)
 
         # An invalid key, raises an exception
-        self.assertRaises(Exception, token.update, {"sshkey": "InvalidKey"})
+        self.assertRaises(TokenAdminError, 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.assertRaises(TokenAdminError, 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.assertRaises(TokenAdminError, 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.assertRaises(TokenAdminError, token.update, {"sshkey": self.unsupported_keytype})
         self.assertEqual(token.rollout_state, ROLLOUTSTATE.BROKEN)
 
         # Set valid key
@@ -86,6 +91,12 @@ def test_01_create_token(self):
         token = SSHkeyTokenClass(db_token)
         token.update({"sshkey": self.sshkey_ed25519})
 
+        # ecdsa_sk
+        db_token = Token(self.serial4, tokentype="sshkey")
+        db_token.save()
+        token = SSHkeyTokenClass(db_token)
+        token.update({"sshkey": self.ecdsa_sk})
+
     def test_02_class_methods(self):
         db_token = Token.query.filter(Token.serial == self.serial1).first()
         token = SSHkeyTokenClass(db_token)
@@ -115,3 +126,9 @@ def test_03_get_sshkey(self):
         sshkey = token.get_sshkey()
         self.assertTrue(sshkey == self.sshkey_ed25519, sshkey)
         self.assertIsInstance(sshkey, six.text_type)
+
+        db_token = Token.query.filter(Token.serial == self.serial4).first()
+        token = SSHkeyTokenClass(db_token)
+        sshkey = token.get_sshkey()
+        self.assertEqual(self.ecdsa_sk, sshkey)
+        self.assertIsInstance(sshkey, six.text_type)