From 23c706fede35e4a7dc327bf407b986be5c3f22d7 Mon Sep 17 00:00:00 2001 From: Viktor Molitsky Date: Tue, 27 Apr 2021 10:12:27 +0300 Subject: [PATCH] OTA-5369 Fix import public keys from pem file --- .../com/advancedtelematic/tuf/cli/Cli.scala | 1 + .../tuf/cli/CommandHandler.scala | 9 +-- .../tuf/cli/repo/CliKeyStorage.scala | 74 ++++++++++++++++--- .../pem-files/invalid-public-RSA.pem | 9 +++ .../pem-files/valid-public-EcPrime256.pem | 4 + .../pem-files/valid-public-Ed25519.pem | 3 + .../resources/pem-files/valid-public-RSA.pem | 9 +++ .../tuf/cli/CliKeyStorageSpec.scala | 43 ++++++++++- 8 files changed, 134 insertions(+), 18 deletions(-) create mode 100644 cli/src/test/resources/pem-files/invalid-public-RSA.pem create mode 100644 cli/src/test/resources/pem-files/valid-public-EcPrime256.pem create mode 100644 cli/src/test/resources/pem-files/valid-public-Ed25519.pem create mode 100644 cli/src/test/resources/pem-files/valid-public-RSA.pem diff --git a/cli/src/main/scala/com/advancedtelematic/tuf/cli/Cli.scala b/cli/src/main/scala/com/advancedtelematic/tuf/cli/Cli.scala index 38c26fd1..9d013fdc 100644 --- a/cli/src/main/scala/com/advancedtelematic/tuf/cli/Cli.scala +++ b/cli/src/main/scala/com/advancedtelematic/tuf/cli/Cli.scala @@ -187,6 +187,7 @@ object Cli extends App with VersionInfo { .toCommand(ImportPublicKey) .text("Imports a public key and stores it on a configurable location") .children( + repoNameOpt(this), manyKeyNamesOpt(this).text("The path to the public key that you want to add."), opt[Path]("input") .abbr("i") diff --git a/cli/src/main/scala/com/advancedtelematic/tuf/cli/CommandHandler.scala b/cli/src/main/scala/com/advancedtelematic/tuf/cli/CommandHandler.scala index 4d0cf9ce..fd132f80 100644 --- a/cli/src/main/scala/com/advancedtelematic/tuf/cli/CommandHandler.scala +++ b/cli/src/main/scala/com/advancedtelematic/tuf/cli/CommandHandler.scala @@ -283,10 +283,9 @@ object CommandHandler { .toFuture case ImportPublicKey => - CliKeyStorage.readPublicKey(config.inputPath.valueOrConfigError).flatMap { key => - config.keyNames.map { keyName => - userKeyStorage.writePublicKey(keyName, key) - }.sequence_ - } + CliKeyStorage + .forRepo(tufRepo.repoPath) + .importPublicKey(config.inputPath.valueOrConfigError, config.keyNames) + .toFuture } } diff --git a/cli/src/main/scala/com/advancedtelematic/tuf/cli/repo/CliKeyStorage.scala b/cli/src/main/scala/com/advancedtelematic/tuf/cli/repo/CliKeyStorage.scala index 4dc03cbe..75caa496 100644 --- a/cli/src/main/scala/com/advancedtelematic/tuf/cli/repo/CliKeyStorage.scala +++ b/cli/src/main/scala/com/advancedtelematic/tuf/cli/repo/CliKeyStorage.scala @@ -1,17 +1,21 @@ package com.advancedtelematic.tuf.cli.repo -import java.nio.file.attribute.{PosixFilePermission, PosixFilePermissions} -import java.nio.file.{FileAlreadyExistsException, Files, Path} -import PosixFilePermission._ -import scala.collection.JavaConverters._ -import com.advancedtelematic.libtuf.data.TufDataType.{KeyType, TufKey, TufKeyPair, TufPrivateKey} +import com.advancedtelematic.libtuf.data.TufCodecs._ +import com.advancedtelematic.libtuf.data.TufDataType.{EcPrime256KeyType, Ed25519KeyType, KeyType, RsaKeyType, TufKey, TufKeyPair, TufPrivateKey} import com.advancedtelematic.tuf.cli.DataType.KeyName +import io.circe.jawn._ +import io.circe.syntax._ +import net.i2p.crypto.eddsa.Utils +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.openssl.{PEMKeyPair, PEMParser} import org.slf4j.LoggerFactory -import scala.util.Try -import com.advancedtelematic.libtuf.data.TufCodecs._ -import io.circe.syntax._ -import io.circe.jawn._ +import java.io.StringReader +import java.nio.file.attribute.PosixFilePermission._ +import java.nio.file.attribute.{PosixFilePermission, PosixFilePermissions} +import java.nio.file.{FileAlreadyExistsException, Files, Path} +import scala.collection.JavaConverters._ +import scala.util.{Failure, Success, Try} object CliKeyStorage { @@ -65,7 +69,7 @@ class CliKeyStorage private (root: Path) { for { _ <- ensureKeysDirCreated() _ <- writePublic(name, pub) - _ = log.info(s"Saved public key to ${root.relativize(name.publicKeyPath)}}") + _ = log.info(s"Saved public key to ${root.relativize(name.publicKeyPath)}") } yield () } @@ -95,4 +99,54 @@ class CliKeyStorage private (root: Path) { pub <- readPublicKey(keyName) priv <- readPrivateKey(keyName) } yield (pub, priv) + + def importPublicKey(pemPath: Path, keyNames: List[KeyName]): Try[Unit] = { + import cats.instances.list._ + import cats.instances.try_._ + import cats.syntax.traverse._ + + for { + key <- readPublicKeyPem(pemPath) + _ <- keyNames.traverse(keyName => writePublicKey(keyName, key)) + } yield () + } + + private def readPublicKeyPem(path: Path): Try[TufKey] = { + val tryReadPemFile = Try { + val source = scala.io.Source.fromFile(path.toFile) + val pem = source.mkString + source.close() + pem + } + + val pemToPublicKeyInfo = (pem: String) => Try { + val parser = new PEMParser(new StringReader(pem)) + parser.readObject() match { + case key: SubjectPublicKeyInfo => key + case keyPair: PEMKeyPair => keyPair.getPublicKeyInfo + } + } + + val tryParseRSA = (pem: String) => + RsaKeyType.crypto.parsePublic(pem) + + val tryParseEcPrime256 = (pem: String) => + pemToPublicKeyInfo(pem) + .map(k => Utils.bytesToHex(k.getEncoded)) + .flatMap(EcPrime256KeyType.crypto.parsePublic) + + val tryParseEd25519 = (pem: String) => + pemToPublicKeyInfo(pem) + .map(k => Utils.bytesToHex(k.getPublicKeyData.getBytes)) + .flatMap(Ed25519KeyType.crypto.parsePublic) + + val publicKeyTry = (pem: String) => + tryParseRSA(pem).orElse(tryParseEcPrime256(pem)).orElse(tryParseEd25519(pem)) + .transform(key => Success(key), _ => Failure(new Exception(s"Cannot parse public key from $path"))) + + for { + pem <- tryReadPemFile + publicKey <- publicKeyTry(pem) + } yield publicKey + } } diff --git a/cli/src/test/resources/pem-files/invalid-public-RSA.pem b/cli/src/test/resources/pem-files/invalid-public-RSA.pem new file mode 100644 index 00000000..33db0e13 --- /dev/null +++ b/cli/src/test/resources/pem-files/invalid-public-RSA.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwSXQGAldIT62ONHQALG1 +N8itakru/T+OvMuOsaDFWuE1uNigXlkULWxonv2ArMcPSM9pQWKFEstGnpmV3dAB +N1Y5crExhAcXLvQqQm4J20BqZRwW2bldBZMofvxrlkBC+x2gkTyi3p1DIdC9nQd/ +8sE5thkU5OAHCuszI2axnJI6geh6riRG/QGJtdvCWc6tYBl78RWiK8m1n2w7olRd +CUK4vpxGhoYSkwHe8Qmhx7HMp41XxTcrX7P1pa1p0vBgZqp6n4O8ixOrJJUWb4QV +Iu4RVdyj/4bGUJdArsQvhRi0w+VjSoyZJ4ASE+4tV0NiknRHZNbWrnifVSzLUee3 +/wIDAQAQqwewqeqweqweqwweqweqwe +-----END PUBLIC KEY----- diff --git a/cli/src/test/resources/pem-files/valid-public-EcPrime256.pem b/cli/src/test/resources/pem-files/valid-public-EcPrime256.pem new file mode 100644 index 00000000..d6a06845 --- /dev/null +++ b/cli/src/test/resources/pem-files/valid-public-EcPrime256.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq5dZn4FXJrkVCFMRDmlRswBF8yMA +/osWy2Vftnbckj+iiShMWav5WtUhtilMSLMJ7vc9XUao6vQJSVWi6HWurQ== +-----END PUBLIC KEY----- diff --git a/cli/src/test/resources/pem-files/valid-public-Ed25519.pem b/cli/src/test/resources/pem-files/valid-public-Ed25519.pem new file mode 100644 index 00000000..e67a7012 --- /dev/null +++ b/cli/src/test/resources/pem-files/valid-public-Ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEANA4Sy6xDvVpQOuwgjBcm7QLvkZbTmcsqLcYcsvpM1o0= +-----END PUBLIC KEY----- diff --git a/cli/src/test/resources/pem-files/valid-public-RSA.pem b/cli/src/test/resources/pem-files/valid-public-RSA.pem new file mode 100644 index 00000000..fa0dde25 --- /dev/null +++ b/cli/src/test/resources/pem-files/valid-public-RSA.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwSXQGAldIT62ONHQALG1 +N8itakru/T+OvMuOsaDFWuE1uNigXlkULWxonv2ArMcPSM9pQWKFEstGnpmV3dAB +N1Y5crExhAcXLvQqQm4J20BqZRwW2bldBZMofvxrlkBC+x2gkTyi3p1DIdC9nQd/ +8sE5thkU5OAHCuszI2axnJI6geh6riRG/QGJtdvCWc6tYBl78RWiK8m1n2w7olRd +CUK4vpxGhoYSkwHe8Qmhx7HMp41XxTcrX7P1pa1p0vBgZqp6n4O8ixOrJJUWb4QV +Iu4RVdyj/4bGUJdArsQvhRi0w+VjSoyZJ4ASE+4tV0NiknRHZNbWrnifVSzLUee3 +/wIDAQAB +-----END PUBLIC KEY----- diff --git a/cli/src/test/scala/com/advancedtelematic/tuf/cli/CliKeyStorageSpec.scala b/cli/src/test/scala/com/advancedtelematic/tuf/cli/CliKeyStorageSpec.scala index 22e27761..0e808b4c 100644 --- a/cli/src/test/scala/com/advancedtelematic/tuf/cli/CliKeyStorageSpec.scala +++ b/cli/src/test/scala/com/advancedtelematic/tuf/cli/CliKeyStorageSpec.scala @@ -1,16 +1,18 @@ package com.advancedtelematic.tuf.cli -import java.nio.file.Files +import com.advancedtelematic.libtuf.data.TufDataType.{EcPrime256KeyType, Ed25519KeyType, RsaKeyType} + +import java.nio.file.{Files, Paths} import java.nio.file.attribute.PosixFilePermission import PosixFilePermission._ - import com.advancedtelematic.tuf.cli.DataType.KeyName import com.advancedtelematic.tuf.cli.repo.CliKeyStorage import com.advancedtelematic.tuf.cli.util.{CliSpec, KeyTypeSpecSupport} +import org.scalatest.TryValues import scala.collection.JavaConverters._ -class CliKeyStorageSpec extends CliSpec with KeyTypeSpecSupport { +class CliKeyStorageSpec extends CliSpec with KeyTypeSpecSupport with TryValues { val tempDir = Files.createTempDirectory("tuf-keys") lazy val subject = CliKeyStorage.forRepo(tempDir) @@ -40,4 +42,39 @@ class CliKeyStorageSpec extends CliSpec with KeyTypeSpecSupport { val perms = Files.getPosixFilePermissions(tempDir.resolve("keys")) perms.asScala shouldBe Set(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE) } + + test("import RSA public key should work") { + val rsaPublicKeyPem = Paths.get(this.getClass.getResource("/pem-files/valid-public-RSA.pem").toURI) + val keyName = KeyName("test-rsa-key") + + subject.readPublicKey(keyName).failure.exception.getMessage should include ("No such file or directory") + subject.importPublicKey(rsaPublicKeyPem, List(keyName)).isSuccess shouldBe true + subject.readPublicKey(keyName).success.value.keytype shouldBe RsaKeyType + } + + test("import Ed25519 public key should work") { + val ed25519PublicKeyPem = Paths.get(this.getClass.getResource("/pem-files/valid-public-Ed25519.pem").toURI) + val keyName = KeyName("test-Ed25519-key") + + subject.readPublicKey(keyName).failure.exception.getMessage should include ("No such file or directory") + subject.importPublicKey(ed25519PublicKeyPem, List(keyName)).isSuccess shouldBe true + subject.readPublicKey(keyName).success.value.keytype shouldBe Ed25519KeyType + } + + test("import EcPrime256 public key should work") { + val ecPrime256PublicKeyPem = Paths.get(this.getClass.getResource("/pem-files/valid-public-EcPrime256.pem").toURI) + val keyName = KeyName("test-EcPrime256-key") + + subject.readPublicKey(keyName).failure.exception.getMessage should include ("No such file or directory") + subject.importPublicKey(ecPrime256PublicKeyPem, List(keyName)).isSuccess shouldBe true + subject.readPublicKey(keyName).success.value.keytype shouldBe EcPrime256KeyType + } + + test("import invalid public key should fail") { + val rsaPublicKeyPem = Paths.get(this.getClass.getResource("/pem-files/invalid-public-RSA.pem").toURI) + val keyName = KeyName("test-invalid-rsa-key") + + subject.readPublicKey(keyName).failure.exception.getMessage should include ("No such file or directory") + subject.importPublicKey(rsaPublicKeyPem, List(keyName)).failure.exception.getMessage should include (s"Cannot parse public key from $rsaPublicKeyPem") + } }