~jan0sch/smederee

Showing details for patch 72d8e10511a63dee4337be05bdff26523ee8e5ff.
2022-10-13 (Thu), 2:30 PM - Jens Grassel - 72d8e10511a63dee4337be05bdff26523ee8e5ff

SSH: Start ssh key management for users.

- BREAKING: add `key_type` column to ssh table
- prepare some data types
- tests and test keys
Summary of changes
9 files added
  • modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala
  • modules/hub/src/test/resources/de/smederee/ssh/ssh-key-with-comment.pub
  • modules/hub/src/test/resources/de/smederee/ssh/ssh-key-without-comment.pub
  • modules/hub/src/test/resources/de/smederee/ssh/test-ssh-dsa.pub
  • modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ecdsa.pub
  • modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ed25519.pub
  • modules/hub/src/test/resources/de/smederee/ssh/test-ssh-rsa.pub
  • modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala
  • modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala
1 files modified with 2 lines added and 0 lines removed
  • modules/hub/src/main/resources/db/migration/V1__base_tables.sql with 2 added and 0 removed lines
diff -rN -u old-smederee/modules/hub/src/main/resources/db/migration/V1__base_tables.sql new-smederee/modules/hub/src/main/resources/db/migration/V1__base_tables.sql
--- old-smederee/modules/hub/src/main/resources/db/migration/V1__base_tables.sql	2025-02-02 04:05:36.562634927 +0000
+++ new-smederee/modules/hub/src/main/resources/db/migration/V1__base_tables.sql	2025-02-02 04:05:36.562634927 +0000
@@ -60,6 +60,7 @@
 (
   "id"          UUID                     NOT NULL, 
   "uid"         UUID                     NOT NULL,
+  "key_type"    CHARACTER VARYING(32)    NOT NULL,
   "key"         TEXT                     NOT NULL,
   "fingerprint" CHARACTER VARYING(256)   NOT NULL,
   "comment"     CHARACTER VARYING(256)   DEFAULT NULL,
@@ -76,6 +77,7 @@
 COMMENT ON TABLE "ssh_keys" IS 'SSH keys uploaded by users live within this table. Updates are not intended, so keys have to be deleted and re-uploaded upon changes.';
 COMMENT ON COLUMN "ssh_keys"."id" IS 'The globally unique ID of the ssh key.';
 COMMENT ON COLUMN "ssh_keys"."uid" IS 'The unique ID of the user account to whom the ssh key belongs.';
+COMMENT ON COLUMN "ssh_keys"."key_type" IS 'The type of the key e.g. ssh-rsa or ssh-ed25519 and so on.';
 COMMENT ON COLUMN "ssh_keys"."key" IS 'A base 64 string containing the public ssh key.';
 COMMENT ON COLUMN "ssh_keys"."fingerprint" IS 'The fingerprint of the ssh key. It must be unique because a key can only be used by one account.';
 COMMENT ON COLUMN "ssh_keys"."comment" IS 'An optional comment for the ssh key limited to 256 characters.';
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala	2025-02-02 04:05:36.566634932 +0000
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022  Contributors as noted in the AUTHORS.md file
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.smederee.ssh
+
+import java.security.PublicKey
+import java.util.UUID
+
+import cats._
+import cats.syntax.all._
+import de.smederee.hub._
+
+import scala.util.matching.Regex
+
+opaque type SshPublicKeyString = String
+object SshPublicKeyString {
+  val Format: Regex = "^([\\w-]+)\\s(.+)(\\s(.+))?$".r
+
+  /** Create an instance of SshPublicKeyString from the given String type.
+    *
+    * @param source
+    *   An instance of type String which will be returned as a SshPublicKeyString.
+    * @return
+    *   The appropriate instance of SshPublicKeyString.
+    */
+  def apply(source: String): SshPublicKeyString = source
+
+  /** Try to create an instance of SshPublicKeyString from the given String.
+    *
+    * @param source
+    *   A String that should fulfil the requirements to be converted into a SshPublicKeyString.
+    * @return
+    *   An option to the successfully converted SshPublicKeyString.
+    */
+  def from(source: String): Option[SshPublicKeyString] = Option(source).filter(string => Format.matches(string))
+}
+
+/** Possible ssh key types.
+  */
+enum SshKeyType {
+  case SshDsa
+
+  case SshEcDsa
+
+  case SshEcDsaSk
+
+  /** Recommended key type because smaller payload and more secure but some servers still don't support them.
+    */
+  case SshEd25519
+
+  case SshEd25519Sk
+
+  /** RSA keys are (still) the most common ones and the most compatible.
+    */
+  case SshRsa
+}
+
+object SshKeyType {
+  val Mappings: Map[String, SshKeyType] = Map(
+    "ssh-dss"             -> SshDsa,
+    "ecdsa-sha2-nistp256" -> SshEcDsa,
+    "ssh-ed25519"         -> SshEd25519,
+    "ssh-rsa"             -> SshRsa
+  )
+
+  given Eq[SshKeyType] = Eq.fromUniversalEquals
+
+  /** Try to extract a supported [[SshKeyType]] from the given string that should contain a valid ssh public key.
+    * According to RFC 4253 the key format is as follows:
+    * {{{
+    * [type-name] [base-64-encoded-public-key] [comment]
+    * }}}
+    * While the comment is optional the other parts are required.
+    *
+    * @param sshKeyString
+    *   A string containing a correctly formatted public ssh key.
+    * @return
+    *   An option to the extracted key type.
+    */
+  def from(sshKeyString: String): Option[SshKeyType] = {
+    val typeString = sshKeyString.takeWhile(char => !char.isWhitespace)
+    Mappings.get(typeString)
+  }
+}
+
+/** Actual ssh key and related metadata describing the public ssh key of a user.
+  *
+  * @param id
+  *   The globally unique id of the ssh key.
+  * @param ownerId
+  *   The unique id of the user account associated with this key.
+  * @param keyType
+  *   The type of the key (rsa, ed25519, ...).
+  * @param key
+  *   The actual key (base 64 encoded).
+  * @param fingerprint
+  *   The fingerprint of the key.
+  * @param comment
+  *   An optional comment for the key, quite often this is an email address.
+  */
+final case class PublicSshKey(
+    id: UUID,
+    ownerId: UserId,
+    keyType: SshKeyType,
+    key: String,
+    fingerprint: String,
+    comment: Option[String]
+)
diff -rN -u old-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-with-comment.pub new-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-with-comment.pub
--- old-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-with-comment.pub	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-with-comment.pub	2025-02-02 04:05:36.566634932 +0000
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8ZU2xquZvstbesPktthwY2r5sanULBQKuM5bGHVdeP Some optional comment...
diff -rN -u old-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-without-comment.pub new-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-without-comment.pub
--- old-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-without-comment.pub	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/test/resources/de/smederee/ssh/ssh-key-without-comment.pub	2025-02-02 04:05:36.566634932 +0000
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBAKn1DHh6DaIg/cN6vNVh1VXvHhH86eKelfsolIfvTPQSb3vkqoWPG3T3DGmrUjbqvrrfzaKILTBRv05KqMCbJKETGR0fuY7G1/Nkd/6dZjw1ngYkGd0fr2ERGuq87+gdd1A3TeIqvdjnl7MG3bEGf+fIEJOrRJraZ+u/tDFlSYq/AAAAFQCAUrv94uu1dVTTiyoagKV4Y4QWuQAAAIAuR5mFFYAgT1+t1u16eRCou1nPO4+q35/6uNNCyXtP0BmZaxXqQw25foJz5OzSQWXjjianfRfUyjsHt5DgM0PAIZaqmxMUiVw7BT7zUTa7ucl9NQmFBexiedCtokVb8++vHVZ7Y42tf2CpqVW8T2lw5b8sWb7rHYGarI935qv2bgAAAIABfRnu0PkvysY6QJhUCD4ZKt3qZ6E1cYDivLhDb4GAZxmxSeN5cFPXU3Gst0oNmNjUW55rsZwZP+KkXi3NwAsTd9dZBxkcc+28m8Dr4hGtPTnPp+4p8wzw/X6Lmyr6RSykCK6xuv9rc2td+1fgNyPoWwcLZZQclDj+OdgQVHWj3A== 
diff -rN -u old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-dsa.pub new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-dsa.pub
--- old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-dsa.pub	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-dsa.pub	2025-02-02 04:05:36.566634932 +0000
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBALzQVYAbt4a9tfNwqg741R8qlqbwU3ZgJ5BBccOeFqr8FCWTUUp7azKAgoz7FeaDOuRJinbrWhPYo33RQpmKsA/Sq71mq42qA3hRYWUYJOdMjCyvqy2eosLrpEspk5jNn6qB4GabJbLtEfJGink3uucZd472TwmAZzzI4T3c6dOrAAAAFQDQtwJHzk6co5ghipxec1kDhlvH9wAAAIEAo4UKmBnJ8DL2br23FsaLTKshNXNGj4taA5IUvbn89iVN5m9O9IfV7Y2i8gh2sbxfhU9AiXd4ABrg3FcXhWfxXI2geI/uc1mKLePlrf9Bjbb9nLnRO8arQBnXkeVb2weNG/PqzyC3PhPCeDTUSkjFK17V8ljhwhn/vcHeLbkl9rgAAACBAJmpzvFNUKR2ARN4cMbcZ1DMPfw6FmtBlOI1aOtlfTfrfObHL5nwFfwtgOoZQ/TA5/dsfJwjbrC/86orLPuiTynLOhcMdD48tervh/6gKzfrQZskAHxDTi0JttDU7KGacmL26Xm5c27qHni5PkQzCbLPXI2pFWPCBkr8fNb32HmV This is a test key.
diff -rN -u old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ecdsa.pub new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ecdsa.pub
--- old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ecdsa.pub	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ecdsa.pub	2025-02-02 04:05:36.566634932 +0000
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBv+4uZH1eefZQ0b1eWb8rduh6G4QMvRh7M9hf39Mzj53LtLD2g6ZmhGn0EmwzgV1gVAvgrG8d969/mF57X2bn8= This is a test key.
diff -rN -u old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ed25519.pub new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ed25519.pub
--- old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ed25519.pub	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-ed25519.pub	2025-02-02 04:05:36.566634932 +0000
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINxizqtw+eIJrou8QKSJevTh+Zpbp0OQPHZUpVNbCOJM This is a test key.
diff -rN -u old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-rsa.pub new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-rsa.pub
--- old-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-rsa.pub	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/test/resources/de/smederee/ssh/test-ssh-rsa.pub	2025-02-02 04:05:36.566634932 +0000
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCm9QvkgYHm3eIDLbMgfvEtre3ByAaTwOoDv+8kqGjBqJTU8JsL1LhQndIloiLzAfwTvXfR02u2yvNCTdvy3iiaB90eEO6O2UgRudNHro++Lhqgkwn0aVjha4cjtX8kjszAeCqIsx42Z+r1NwA3wSjQe0ytKZn95dkLGkTVaMqfHaQXjYfqP5vNBugQcbYQv2m/wVzqSjJC9uW/3w7C5hXJK78I+33S86MaTK9g1RSp2zakgokLE7XBWq/gX4CcG0EAWnstoSbjsIQ+ODv1F4V3D8gzTSszKV4EzB95PeOW2XT80RPnljhGCSkO1qo/HrrvpMhWGIDYI3e1i+P9EKmpMsi7FNJNWGLVh+d+DTFWs0WPlqRKzjj+87vrHMcd9W3q1vfnuQRp7RMuEm1r2IUcqsThUqeHEQwKnBMFxnE1MiqZwNdIEodikUoETB0BuD1Shj5FCu18KOAqowN2/NJ1HRllnVNd457h68HwQOLY8A8q51CrjVODZ4FIeTxdsHU= This is a test key.
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala	2025-02-02 04:05:36.566634932 +0000
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022  Contributors as noted in the AUTHORS.md file
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.smederee.ssh
+
+import munit._
+
+final class PublicSshKeyTest extends FunSuite {
+  test("SshKeyString must work for keys with comment") {
+    val input = scala.io.Source
+      .fromInputStream(
+        getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-with-comment.pub"),
+        "UTF-8"
+      )
+      .getLines()
+      .mkString
+    val expected = SshPublicKeyString(input)
+    SshPublicKeyString.from(input) match {
+      case None            => fail("Key could not be parsed!")
+      case Some(keyString) => assertEquals(keyString, expected)
+    }
+  }
+
+  test("SshKeyString must work for keys without comment") {
+    val input = scala.io.Source
+      .fromInputStream(
+        getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-without-comment.pub"),
+        "UTF-8"
+      )
+      .getLines()
+      .mkString
+    val expected = SshPublicKeyString(input)
+    SshPublicKeyString.from(input) match {
+      case None            => fail("Key could not be parsed!")
+      case Some(keyString) => assertEquals(keyString, expected)
+    }
+  }
+}
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala	2025-02-02 04:05:36.566634932 +0000
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022  Contributors as noted in the AUTHORS.md file
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.smederee.ssh
+
+import munit._
+
+final class SshKeyTypeTest extends FunSuite {
+  test("from must work for ssh-dsa") {
+    val input = scala.io.Source
+      .fromInputStream(getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-dsa.pub"), "UTF-8")
+      .getLines()
+      .mkString
+    assertEquals(
+      SshKeyType.from(input),
+      Some(SshKeyType.SshDsa),
+      s"Incorrect key type extracted from: $input"
+    )
+  }
+
+  test("from must work for ssh-ecdsa") {
+    val input = scala.io.Source
+      .fromInputStream(getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-ecdsa.pub"), "UTF-8")
+      .getLines()
+      .mkString
+    assertEquals(
+      SshKeyType.from(input),
+      Some(SshKeyType.SshEcDsa),
+      s"Incorrect key type extracted from: $input"
+    )
+  }
+
+  test("from must work for ssh-ed25519") {
+    val input = scala.io.Source
+      .fromInputStream(getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-ed25519.pub"), "UTF-8")
+      .getLines()
+      .mkString
+    assertEquals(
+      SshKeyType.from(input),
+      Some(SshKeyType.SshEd25519),
+      s"Incorrect key type extracted from: $input"
+    )
+  }
+
+  test("from must work for ssh-rsa") {
+    val input = scala.io.Source
+      .fromInputStream(getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-rsa.pub"), "UTF-8")
+      .getLines()
+      .mkString
+    assertEquals(
+      SshKeyType.from(input),
+      Some(SshKeyType.SshRsa),
+      s"Incorrect key type extracted from: $input"
+    )
+  }
+
+}