From b95d67d87e3795e0246da51108a7d46b85b53036 Mon Sep 17 00:00:00 2001 From: Simao Mata Date: Tue, 30 May 2023 12:04:55 +0100 Subject: [PATCH 1/6] Add new api to rotate root.jon Added keyserver endpoint to rotate json. Generates new root and targets keys, replaces the previous keys in root.json and signs the new root.json with both the old and new root keys. - PUT `/api/v1/root/:repo-id/rotate` Request body is ignored. Added reposerver endpoint delegating the root.json rotation to keyserver and forcing a re generation of targets.json and associated metadata with the new keys. - PUT `/api/v1/repo/:repo-id/root/rotate` - PUT `/api/v1/user_repo/root/rotate` --- .../keyserver/data/KeyServerDataType.scala | 30 ++++----- .../tuf/keyserver/db/Repository.scala | 31 ++++++--- .../tuf/keyserver/http/Errors.scala | 2 + .../tuf/keyserver/http/RootRoleResource.scala | 7 +- .../keyserver/http/TufKeyserverRoutes.scala | 2 +- .../roles/KeyGenerationRequests.scala | 4 +- .../tuf/keyserver/roles/RoleSigning.scala | 13 +++- .../tuf/keyserver/roles/RootRoleKeyEdit.scala | 67 ++++++++++++++++--- .../keyserver/http/RootRoleResourceSpec.scala | 53 ++++++++++++++- .../keyserver/KeyserverClient.scala | 7 ++ .../libtuf/data/ErrorCodes.scala | 2 + .../tuf/reposerver/http/RepoResource.scala | 18 +++++ .../reposerver/http/RepoResourceSpec.scala | 32 +++++++++ .../tuf/reposerver/util/ResourceSpec.scala | 8 +++ 14 files changed, 232 insertions(+), 44 deletions(-) diff --git a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/data/KeyServerDataType.scala b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/data/KeyServerDataType.scala index 51cd72ad..e4c84478 100644 --- a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/data/KeyServerDataType.scala +++ b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/data/KeyServerDataType.scala @@ -1,19 +1,13 @@ package com.advancedtelematic.tuf.keyserver.data -import java.security.PublicKey -import java.time.Instant -import java.util.UUID - import com.advancedtelematic.libats.data.UUIDKey.{UUIDKey, UUIDKeyObj} -import com.advancedtelematic.libats.slick.db.SlickEncryptedColumn.EncryptedColumn -import com.advancedtelematic.libtuf.data.ClientDataType.RootRole +import com.advancedtelematic.libtuf.data.ClientDataType.{RootRole, TufRole} import com.advancedtelematic.libtuf.data.TufDataType.RoleType.RoleType -import com.advancedtelematic.libtuf.data.TufDataType.{KeyId, KeyType, RepoId, JsonSignedPayload, SignedPayload, TufKey, TufKeyPair, TufPrivateKey} +import com.advancedtelematic.libtuf.data.TufDataType.{KeyId, KeyType, RepoId, SignedPayload, TufKey, TufKeyPair, TufPrivateKey} import com.advancedtelematic.tuf.keyserver.data.KeyServerDataType.KeyGenRequestStatus.KeyGenRequestStatus -import com.advancedtelematic.tuf.keyserver.http.Errors -import io.circe.{Decoder, Json} -import scala.concurrent.Future +import java.time.Instant +import java.util.UUID import scala.util.Try object KeyServerDataType { @@ -35,13 +29,9 @@ object KeyServerDataType { require(keyType.crypto.validKeySize(keySize), s"Invalid keysize ($keySize) for $keyType") } - object SignedRootRole { - import com.advancedtelematic.libtuf.data.ClientCodecs._ - - def fromSignedPayload(repoId: RepoId, payload: SignedPayload[RootRole]): SignedRootRole = { - val content = SignedPayload(payload.signatures, payload.signed, payload.json) - SignedRootRole(repoId, content, payload.signed.expires, payload.signed.version) - } + implicit class SignedPayloadDbOps(value: SignedPayload[RootRole]) { + def toDbSignedRole(repoId: RepoId): SignedRootRole = + SignedRootRole(repoId, value, value.signed.expires, value.signed.version) } case class SignedRootRole(repoId: RepoId, content: SignedPayload[RootRole], expiresAt: Instant, version: Int) @@ -49,4 +39,10 @@ object KeyServerDataType { case class Key(id: KeyId, repoId: RepoId, roleType: RoleType, keyType: KeyType, publicKey: TufKey, privateKey: TufPrivateKey) { def toTufKeyPair: Try[TufKeyPair] = keyType.crypto.castToKeyPair(publicKey, privateKey) } + + implicit class TufKeyDbOps(value: TufKeyPair) { + def toDbKey(repoId: RepoId, roleType: RoleType): Key = { + Key(value.pubkey.id, repoId, roleType, value.pubkey.keytype, value.pubkey, value.privkey) + } + } } diff --git a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/db/Repository.scala b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/db/Repository.scala index 36e46379..28906f4a 100644 --- a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/db/Repository.scala +++ b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/db/Repository.scala @@ -1,18 +1,18 @@ package com.advancedtelematic.tuf.keyserver.db -import com.advancedtelematic.libtuf.data.TufDataType._ -import com.advancedtelematic.tuf.keyserver.data.KeyServerDataType._ -import com.advancedtelematic.tuf.keyserver.data.KeyServerDataType.KeyGenRequestStatus -import com.advancedtelematic.tuf.keyserver.data.KeyServerDataType.KeyGenRequestStatus.KeyGenRequestStatus import com.advancedtelematic.libats.http.Errors.{EntityAlreadyExists, MissingEntity} import com.advancedtelematic.libats.slick.codecs.SlickRefined._ -import com.advancedtelematic.libtuf.data.ClientDataType.RootRole -import com.advancedtelematic.libats.slick.db.SlickUUIDKey._ import com.advancedtelematic.libats.slick.db.SlickExtensions._ +import com.advancedtelematic.libats.slick.db.SlickUUIDKey._ +import com.advancedtelematic.libtuf.data.ClientDataType.RootRole +import com.advancedtelematic.libtuf.data.TufDataType.RoleType.RoleType +import com.advancedtelematic.libtuf.data.TufDataType._ +import com.advancedtelematic.tuf.keyserver.data.KeyServerDataType.KeyGenRequestStatus.KeyGenRequestStatus +import com.advancedtelematic.tuf.keyserver.data.KeyServerDataType.{KeyGenRequestStatus, _} +import com.advancedtelematic.tuf.keyserver.db.SlickMappings._ import slick.jdbc.MySQLProfile.api._ import scala.concurrent.{ExecutionContext, Future} -import SlickMappings._ trait DatabaseSupport { implicit val ec: ExecutionContext @@ -98,15 +98,12 @@ object KeyRepository { } protected [db] class KeyRepository()(implicit db: Database, ec: ExecutionContext) { - import Schema.keys import KeyRepository._ + import Schema.keys import com.advancedtelematic.libats.slick.db.SlickPipeToUnit.pipeToUnit def persist(key: Key): Future[Unit] = db.run(persistAction(key)) - protected [db] def deleteRepoKeys(repoId: RepoId, keysToDelete: Set[KeyId]): DBIO[Unit] = - keys.filter(_.repoId === repoId).filter(_.id.inSet(keysToDelete)).delete.map(_ => ()) - protected [db] def keepOnlyKeys(repoId: RepoId, keysToKeep: Set[KeyId]): DBIO[Unit] = keys.filter(_.repoId === repoId).filterNot(_.id.inSet(keysToKeep)).delete.map(_ => ()) @@ -154,6 +151,18 @@ protected[db] class SignedRootRoleRepository()(implicit db: Database, ec: Execut keyRepository.keepOnlyKeys(signedRootRole.repoId, keysToKeep).andThen(persistAction(signedRootRole).transactionally) } + def persistWithKeys(keyRepository: KeyRepository)(signedRootRole: SignedRootRole, + newKeys: Map[RoleType, List[TufKeyPair]]): Future[Unit] = db.run { + val keys = newKeys + .flatMap { case (roleType, keyPairs) => keyPairs.map(_.toDbKey(signedRootRole.repoId, roleType)) } + .toSeq + + DBIO.seq( + keyRepository.persistAllAction(keys), + persistAction(signedRootRole) + ).transactionally + } + protected [db] def persistAction(signedRootRole: SignedRootRole): DBIO[Unit] = { (signedRootRoles += signedRootRole) .handleIntegrityErrors(RootRoleExists) diff --git a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/http/Errors.scala b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/http/Errors.scala index 980d1b7b..ae47ce61 100644 --- a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/http/Errors.scala +++ b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/http/Errors.scala @@ -13,6 +13,8 @@ object Errors { val RepoRootKeysNotFound = RawError(ErrorCodes.KeyServer.RepoRootKeysNotFound, StatusCodes.NotFound, "Repository root keys not available/offline") val RoleKeysNotFound = RawError(ErrorCodes.KeyServer.RoleKeysNotFound, StatusCodes.NotFound, "There are no keys for this repoid/roletype") val PrivateKeysNotFound = RawError(ErrorCodes.KeyServer.PrivateKeysNotFound, StatusCodes.PreconditionFailed, "There are no private keys for that role") + val KeysOffline = RawError(ErrorCodes.KeyServer.KeysOffline, StatusCodes.PreconditionFailed, "private keys are offline") + val KeysReadError = RawError(ErrorCodes.KeyServer.KeysReadError, StatusCodes.InternalServerError, "private keys could not be read from the database") def KeyGenerationFailed(repoId: RepoId, errors: Map[KeyGenId, String]) = JsonError(ErrorCodes.KeyServer.KeyGenerationFailed, StatusCodes.InternalServerError, errors.asJson, "Could not generate keys") diff --git a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/http/RootRoleResource.scala b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/http/RootRoleResource.scala index 1cb3d1cd..451a1ca1 100644 --- a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/http/RootRoleResource.scala +++ b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/http/RootRoleResource.scala @@ -25,7 +25,7 @@ import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} class RootRoleResource() - (implicit val db: Database, val ec: ExecutionContext, mat: Materializer) + (implicit val db: Database, val ec: ExecutionContext) extends KeyGenRequestSupport with Settings { import ClientRootGenRequest._ import akka.http.scaladsl.server.Directives._ @@ -77,6 +77,11 @@ class RootRoleResource() complete(f) } } ~ + (path("rotate") & put) { + onSuccess(rootRoleKeyEdit.rotate(repoId)) { + complete(StatusCodes.OK) + } + } ~ path("1") { val f = signedRootRoles .findByVersion(repoId, version = 1) diff --git a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/http/TufKeyserverRoutes.scala b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/http/TufKeyserverRoutes.scala index 61590a15..9538de5e 100644 --- a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/http/TufKeyserverRoutes.scala +++ b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/http/TufKeyserverRoutes.scala @@ -15,7 +15,7 @@ import slick.jdbc.MySQLProfile.api._ class TufKeyserverRoutes(dependencyChecks: Seq[HealthCheck] = Seq.empty, metricsRoutes: Route = Directives.reject, metricRegistry: MetricRegistry = MetricsSupport.metricRegistry) - (implicit val db: Database, val ec: ExecutionContext, system: ActorSystem, mat: Materializer) + (implicit val db: Database, val ec: ExecutionContext, mat: Materializer) extends VersionInfo { import Directives._ diff --git a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/roles/KeyGenerationRequests.scala b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/roles/KeyGenerationRequests.scala index c817bb8c..c2896f62 100644 --- a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/roles/KeyGenerationRequests.scala +++ b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/roles/KeyGenerationRequests.scala @@ -99,7 +99,7 @@ extends KeyRepositorySupport with SignedRootRoleSupport { } private def persistSignedPayload(repoId: RepoId)(signedPayload: SignedPayload[RootRole]): Future[SignedRootRole] = { - val signedRootRole = SignedRootRole.fromSignedPayload(repoId, signedPayload) + val signedRootRole = signedPayload.toDbSignedRole(repoId) signedRootRoleRepo.persist(signedRootRole).map(_ => signedRootRole) } @@ -118,7 +118,7 @@ extends KeyRepositorySupport with SignedRootRoleSupport { userSignedIsValid <- offlineSignedParsedV match { case Valid(offlineSignedParsed) => val newOnlineKeys = offlineSignedParsed.signed.keys.values.map(_.id).toSet - val signedRootRole = SignedRootRole.fromSignedPayload(repoId, offlineSignedParsed) + val signedRootRole = offlineSignedParsed.toDbSignedRole(repoId) signedRootRoleRepo.persistAndKeepRepoKeys(keyRepo)(signedRootRole, newOnlineKeys).map(_ => Valid(signedRootRole)) case r@Invalid(_) => diff --git a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/roles/RoleSigning.scala b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/roles/RoleSigning.scala index 8cdaf42b..f2f02586 100644 --- a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/roles/RoleSigning.scala +++ b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/roles/RoleSigning.scala @@ -6,7 +6,7 @@ import com.advancedtelematic.libtuf.data.TufDataType.RoleType.RoleType import com.advancedtelematic.libtuf.data.TufDataType.{RepoId, _} import com.advancedtelematic.tuf.keyserver.db._ import com.advancedtelematic.tuf.keyserver.http.Errors -import io.circe.Json +import io.circe.{Encoder, Json} import slick.jdbc.MySQLProfile.api._ import io.circe.syntax._ @@ -46,6 +46,17 @@ class RoleSigning()(implicit val db: Database, val ec: ExecutionContext) } } + protected [roles] def signWithPrivateKeys[T : Encoder](payload: T, privateKeys: Seq[TufKeyPair]): SignedPayload[T] = { + val payloadJson = payload.asJson + + val signatures = privateKeys.toList.map { key => + val signature = TufCrypto.signPayload(key.privkey, payloadJson) + ClientSignature(key.pubkey.id, signature.method, signature.sig) + } + + SignedPayload(signatures, payload, payloadJson) + } + private def fetchPrivateKey(key: TufKey): Future[TufPrivateKey] = keyRepo.find(key.id).recoverWith { case KeyRepository.KeyNotFound => diff --git a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/roles/RootRoleKeyEdit.scala b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/roles/RootRoleKeyEdit.scala index 0b516b01..69c08c9d 100644 --- a/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/roles/RootRoleKeyEdit.scala +++ b/keyserver/src/main/scala/com/advancedtelematic/tuf/keyserver/roles/RootRoleKeyEdit.scala @@ -1,19 +1,24 @@ package com.advancedtelematic.tuf.keyserver.roles -import com.advancedtelematic.libtuf.data.TufDataType.{KeyId, RepoId, TufKeyPair} -import com.advancedtelematic.tuf.keyserver.db.{KeyRepository, KeyRepositorySupport} +import akka.http.scaladsl.util.FastFuture +import cats.implicits._ +import com.advancedtelematic.libtuf.crypt.TufCrypto +import com.advancedtelematic.libtuf.data.ClientCodecs._ +import com.advancedtelematic.libtuf.data.RootManipulationOps._ +import com.advancedtelematic.libtuf.data.TufDataType.RoleType.RoleType +import com.advancedtelematic.libtuf.data.TufDataType.{KeyId, KeyType, RepoId, RoleType, TufKeyPair} +import com.advancedtelematic.tuf.keyserver.data.KeyServerDataType.SignedPayloadDbOps +import com.advancedtelematic.tuf.keyserver.db.KeyRepository.KeyNotFound +import com.advancedtelematic.tuf.keyserver.db.{KeyRepositorySupport, SignedRootRoleSupport} +import com.advancedtelematic.tuf.keyserver.http.Errors +import slick.jdbc.MySQLProfile.api._ import scala.async.Async.{async, await} import scala.concurrent.{ExecutionContext, Future} -import com.advancedtelematic.tuf.keyserver.db.KeyRepository.KeyNotFound -import com.advancedtelematic.libtuf.data.TufDataType.RoleType.RoleType -import slick.jdbc.MySQLProfile.api._ -import cats.implicits._ -import com.advancedtelematic.libtuf.data.RootManipulationOps._ class RootRoleKeyEdit() (implicit val db: Database, val ec: ExecutionContext) - extends KeyRepositorySupport { + extends KeyRepositorySupport with SignedRootRoleSupport { val roleSigning = new RoleSigning() val signedRootRole = new SignedRootRoles() @@ -22,6 +27,50 @@ class RootRoleKeyEdit() _ <- keyRepo.delete(keyId) } yield () + + def rotate(repoId: RepoId): Future[Unit] = async { + val unsigned = await(signedRootRole.findForSign(repoId)) + var newRoot = unsigned + + val newRootKeys = List.fill(newRoot.roles.get(RoleType.ROOT).map(_.keyids).toList.flatten.size) { + TufCrypto.generateKeyPair(KeyType.default, KeyType.default.crypto.defaultKeySize) + } + + val newTargetsKeys = List.fill(newRoot.roles.get(RoleType.TARGETS).map(_.keyids).toList.flatten.size) { + TufCrypto.generateKeyPair(KeyType.default, KeyType.default.crypto.defaultKeySize) + } + + if (newTargetsKeys.nonEmpty) { + newRoot = newRoot.withRoleKeys(RoleType.TARGETS, newTargetsKeys.map(_.pubkey):_*) + } + + if (newRootKeys.nonEmpty) { + newRoot = newRoot.withRoleKeys(RoleType.ROOT, newRootKeys.map(_.pubkey):_*) + } + + val oldPrivateKeys = await { + keyRepo.findAll(unsigned.roleKeys(RoleType.ROOT).map(_.id)) + .recoverWith { + case KeyNotFound => + FastFuture.failed(Errors.KeysOffline) + } + } + + val oldKeyPairs = oldPrivateKeys.map { k => + k.toTufKeyPair.toEither.valueOr(_ => throw Errors.KeysReadError) + } + + val signedPayload = roleSigning.signWithPrivateKeys(newRoot, newRootKeys ++ oldKeyPairs) + + val newKeys = Map( + RoleType.ROOT -> newRootKeys, + RoleType.TARGETS -> newTargetsKeys, + ) + + await(signedRootRoleRepo.persistWithKeys(keyRepo)(signedPayload.toDbSignedRole(repoId), newKeys)) + } + + def findAllKeyPairs(repoId: RepoId, roleType: RoleType): Future[Seq[TufKeyPair]] = for { rootRole <- signedRootRole.findLatest(repoId) @@ -35,7 +84,7 @@ class RootRoleKeyEdit() for { _ <- ensureIsRepoKey(repoId, keyId) key <- keyRepo.find(keyId) - keyPair ← Future.fromTry(key.toTufKeyPair) + keyPair <- Future.fromTry(key.toTufKeyPair) } yield keyPair } diff --git a/keyserver/src/test/scala/com/advancedtelematic/tuf/keyserver/http/RootRoleResourceSpec.scala b/keyserver/src/test/scala/com/advancedtelematic/tuf/keyserver/http/RootRoleResourceSpec.scala index c5521bdc..edc1c57f 100644 --- a/keyserver/src/test/scala/com/advancedtelematic/tuf/keyserver/http/RootRoleResourceSpec.scala +++ b/keyserver/src/test/scala/com/advancedtelematic/tuf/keyserver/http/RootRoleResourceSpec.scala @@ -314,8 +314,6 @@ class RootRoleResourceSpec extends TufKeyserverSpec } Post(apiUri(s"root/${repoId.show}/targets"), Json.Null) ~> routes ~> check { - println(responseAs[Json].noSpaces) - status shouldBe StatusCodes.PreconditionFailed } } @@ -898,6 +896,57 @@ class RootRoleResourceSpec extends TufKeyserverSpec offlineTargetsKeys.threshold shouldBe 1 } + test("rotate key fails with specific error when keys are offline") { + val repoId = RepoId.generate() + generateRootRole(repoId, Ed25519KeyType).futureValue + + val oldRoot = fetchLatestRootOk(repoId).signed + val rootKeyId = oldRoot.roles(RoleType.ROOT).keyids.headOption.value + + Delete (apiUri(s"root/${repoId.show}/private_keys/${rootKeyId.value}")) ~> routes ~> check { + status shouldBe StatusCodes.NoContent + } + + val error = Put(apiUri(s"root/${repoId.show}/rotate")) ~> routes ~> check { + status shouldBe StatusCodes.PreconditionFailed + responseAs[ErrorRepresentation] + } + + error.code shouldBe ErrorCodes.KeyServer.KeysOffline + } + + test("rotates the key") { + val repoId = RepoId.generate() + generateRootRole(repoId, Ed25519KeyType).futureValue + + val oldRoot = fetchLatestRootOk(repoId).signed + + Put(apiUri(s"root/${repoId.show}/rotate")) ~> routes ~> check { + status shouldBe StatusCodes.OK + } + + val newSignedRoot = fetchLatestRootOk(repoId) + val newRoot = newSignedRoot.signed + + newRoot.version shouldBe oldRoot.version + 1 + + val newKeys = newRoot.roles.filterKeys(r => r == RoleType.ROOT || r == RoleType.TARGETS) + .values.flatMap(_.keyids).toSet + val oldKeys = oldRoot.roles.filterKeys(r => r == RoleType.ROOT || r == RoleType.TARGETS) + .values.flatMap(_.keyids).toSet + + newKeys.intersect(oldKeys) shouldBe empty + + val signedWithKeys = newSignedRoot.signatures.map(_.keyid) + + val oldRootKeys = oldRoot.roles(RoleType.ROOT).keyids + val newRootKeys = newRoot.roles(RoleType.ROOT).keyids + + signedWithKeys should contain allElementsOf oldRootKeys + signedWithKeys should contain allElementsOf newRootKeys + } + + def fetchLatestRootOk(repoId: RepoId): SignedPayload[RootRole] = { Get(apiUri(s"root/${repoId.show}")) ~> routes ~> check { status shouldBe StatusCodes.OK diff --git a/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/keyserver/KeyserverClient.scala b/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/keyserver/KeyserverClient.scala index 911ea96f..7c6b2242 100644 --- a/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/keyserver/KeyserverClient.scala +++ b/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/keyserver/KeyserverClient.scala @@ -40,6 +40,8 @@ trait KeyserverClient { def fetchUnsignedRoot(repoId: RepoId): Future[RootRole] + def rotateRoot(repoId: RepoId): Future[Unit] + def updateRoot(repoId: RepoId, signedPayload: SignedPayload[RootRole]): Future[Unit] def deletePrivateKey(repoId: RepoId, keyId: KeyId): Future[Unit] @@ -153,4 +155,9 @@ class KeyserverHttpClient(uri: Uri, httpClient: HttpRequest => Future[HttpRespon val req = HttpRequest(HttpMethods.PUT, uri = apiUri(Path("root") / repoId.show / "roles" / "remote-sessions")) execHttpUnmarshalled[Unit](req).ok } + + override def rotateRoot(repoId: RepoId): Future[Unit] = { + val req = HttpRequest(HttpMethods.PUT, uri = apiUri(Path("root") / repoId.show / "rotate")) + execHttpUnmarshalled[Unit](req).ok + } } diff --git a/libtuf/src/main/scala/com/advancedtelematic/libtuf/data/ErrorCodes.scala b/libtuf/src/main/scala/com/advancedtelematic/libtuf/data/ErrorCodes.scala index 4898169a..c5e3aff7 100644 --- a/libtuf/src/main/scala/com/advancedtelematic/libtuf/data/ErrorCodes.scala +++ b/libtuf/src/main/scala/com/advancedtelematic/libtuf/data/ErrorCodes.scala @@ -11,6 +11,8 @@ object ErrorCodes { val KeyGenerationFailed = ErrorCode("key_generation_failed") val InvalidRootRole = ErrorCode("invalid_root_role") val SignedRootRoleDecodingFailed = ErrorCode("signed_root_role_decoding_failed") + val KeysOffline = ErrorCode("private_keys_offline") + val KeysReadError = ErrorCode("private_keys_read_error") } object Reposerver { diff --git a/reposerver/src/main/scala/com/advancedtelematic/tuf/reposerver/http/RepoResource.scala b/reposerver/src/main/scala/com/advancedtelematic/tuf/reposerver/http/RepoResource.scala index 03570a40..a587aa7b 100644 --- a/reposerver/src/main/scala/com/advancedtelematic/tuf/reposerver/http/RepoResource.scala +++ b/reposerver/src/main/scala/com/advancedtelematic/tuf/reposerver/http/RepoResource.scala @@ -7,6 +7,7 @@ import akka.http.scaladsl.model.headers.{RawHeader, `Content-Length`} import akka.http.scaladsl.model.{EntityStreamException, HttpEntity, HttpHeader, HttpRequest, HttpResponse, ParsingException, StatusCode, StatusCodes, Uri} import akka.http.scaladsl.server._ import akka.http.scaladsl.unmarshalling._ +import akka.http.scaladsl.util.FastFuture import akka.stream.scaladsl.Source import akka.util.ByteString import cats.data.Validated.{Invalid, Valid} @@ -267,9 +268,26 @@ class RepoResource(keyserverClient: KeyserverClient, namespaceValidation: Namesp _ <- tufTargetsPublisher.newTargetsAdded(namespace, signedPayload.signed.targets, newItems) } yield newSignedRole + private def rotateRoot(repoId: RepoId): Future[StatusCode] = async { + await(keyserverClient.rotateRoot(repoId)) + await { + signedRoleGeneration.regenerateAllSignedRoles(repoId) + .recoverWith { + case err => + log.error("root.json was rotated, but signing targets.json failed", err) + FastFuture.successful(()) + } + } + + StatusCodes.OK + } + private def modifyRepoRoutes(repoId: RepoId): Route = (namespaceValidation(repoId) & withRepoIdHeader(repoId)){ namespace => pathPrefix("root") { + (path("rotate") & put) { + complete(rotateRoot(repoId)) + } ~ pathEnd { get { complete(keyserverClient.fetchUnsignedRoot(repoId)) diff --git a/reposerver/src/test/scala/com/advancedtelematic/tuf/reposerver/http/RepoResourceSpec.scala b/reposerver/src/test/scala/com/advancedtelematic/tuf/reposerver/http/RepoResourceSpec.scala index 14dd74e9..0c9a6092 100644 --- a/reposerver/src/test/scala/com/advancedtelematic/tuf/reposerver/http/RepoResourceSpec.scala +++ b/reposerver/src/test/scala/com/advancedtelematic/tuf/reposerver/http/RepoResourceSpec.scala @@ -1544,6 +1544,38 @@ class RepoResourceSpec extends TufReposerverSpec with RepoResourceSpecUtil } } + test("rotating root generates root and targets") { + val repoId = addTargetToRepo() + + val oldTargets = Get(apiUri(s"repo/${repoId.show}/targets.json")) ~> routes ~> check { + status shouldBe StatusCodes.OK + responseAs[SignedPayload[TargetsRole]].signed + } + + val oldRoot = Get(apiUri(s"repo/${repoId.show}/root.json")) ~> routes ~> check { + status shouldBe StatusCodes.OK + responseAs[SignedPayload[RootRole]].signed + } + + Put(apiUri(s"repo/${repoId.show}/root/rotate")) ~> routes ~> check { + status shouldBe StatusCodes.OK + } + + Get(apiUri(s"repo/${repoId.show}/targets.json")) ~> routes ~> check { + status shouldBe StatusCodes.OK + val updatedRole = responseAs[SignedPayload[TargetsRole]].signed + + updatedRole.version shouldBe oldTargets.version + 1 + } + + Get(apiUri(s"repo/${repoId.show}/root.json")) ~> routes ~> check { + status shouldBe StatusCodes.OK + val updatedRole = responseAs[SignedPayload[RootRole]].signed + + updatedRole.version shouldBe oldRoot.version + 1 + } + } + implicit class ErrorRepresentationOps(value: ErrorRepresentation) { def firstErrorCause: Option[String] = value.cause.flatMap(_.as[NonEmptyList[String]].toOption).map(_.head) diff --git a/reposerver/src/test/scala/com/advancedtelematic/tuf/reposerver/util/ResourceSpec.scala b/reposerver/src/test/scala/com/advancedtelematic/tuf/reposerver/util/ResourceSpec.scala index 38b1d576..4fb51aaa 100644 --- a/reposerver/src/test/scala/com/advancedtelematic/tuf/reposerver/util/ResourceSpec.scala +++ b/reposerver/src/test/scala/com/advancedtelematic/tuf/reposerver/util/ResourceSpec.scala @@ -206,6 +206,14 @@ class FakeKeyserverClient extends KeyserverClient { override def addRemoteSessionsRole(repoId: RepoId): Future[Unit] = addRoles(repoId, RoleType.REMOTE_SESSIONS) + + // Does not actually rotate, but for testing on reposerver is ok. Rotation is done on keyserver + override def rotateRoot(repoId: RepoId): Future[Unit] = async { + val oldRoot = await(fetchRootRole(repoId)).signed + val newRootRole = oldRoot.copy(version = oldRoot.version + 1, expires = oldRoot.expires.plus(1, ChronoUnit.DAYS)) + val signed = await(sign(repoId, newRootRole)) + rootRoles.put(repoId, signed) + } } trait LongHttpRequest { From 13f7fe512ad8d6decf7fb0efcd3fbcfee2a996b7 Mon Sep 17 00:00:00 2001 From: Simao Mata Date: Wed, 21 Jun 2023 15:54:16 +0200 Subject: [PATCH 2/6] bump libtuf in script --- scripts/canonical-json.sc | 2 +- scripts/insert-slow.sc | 36 ++++++++++++++++++++++++++++++++++++ scripts/sign-role.sc | 23 +++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100755 scripts/insert-slow.sc create mode 100644 scripts/sign-role.sc diff --git a/scripts/canonical-json.sc b/scripts/canonical-json.sc index 124c402e..c12bd16c 100755 --- a/scripts/canonical-json.sc +++ b/scripts/canonical-json.sc @@ -1,6 +1,6 @@ #!/usr/bin/env amm -import $ivy.`io.github.uptane::libtuf:1.0.0` +import $ivy.`io.github.uptane::libtuf:3.0.0` import io.circe.{Json, JsonObject} import io.circe.syntax._ diff --git a/scripts/insert-slow.sc b/scripts/insert-slow.sc new file mode 100755 index 00000000..52fbbe4e --- /dev/null +++ b/scripts/insert-slow.sc @@ -0,0 +1,36 @@ +import $ivy.`org.mariadb.jdbc:mariadb-java-client:2.7.4` +import $ivy.`com.lihaoyi::ammonite-ops:2.4.0` + +import ammonite.ops._ + +import scala.concurrent.duration.Duration + +@main +def main(url: String, file: String) = { + val json: String = read! pwd/file + + import java.sql.Connection + import java.sql.DriverManager + val con = DriverManager.getConnection(url) + + val del = "delete from signed_roles where repo_id = '48c7372d-95aa-4dca-aafe-60b30a014dcc' limit 1 ;" + + con.createStatement().execute(del) + + val stm = con.prepareStatement("INSERT INTO signed_roles (repo_id, role_type, checksum, `length`, version, content) values (?, ?, ?, ?, ?,?)") + + stm.setString(1, "48c7372d-95aa-4dca-aafe-60b30a014dcc") + stm.setString(2, "TARGETS") + stm.setString(3, "x") + stm.setLong(4, 0) + stm.setLong(5, -1) + stm.setString(6, json) + + val startAt = System.nanoTime() + + val res = stm.executeUpdate() + + val took = Duration.fromNanos(System.nanoTime() - startAt) + + println(s"Finished: $res took ${took.toMillis}ms") +} diff --git a/scripts/sign-role.sc b/scripts/sign-role.sc new file mode 100644 index 00000000..356f00a7 --- /dev/null +++ b/scripts/sign-role.sc @@ -0,0 +1,23 @@ +#!/usr/bin/env amm + +import $ivy.`com.lihaoyi::ammonite-ops:2.4.0` +import $ivy.`io.github.uptane::libtuf:1.0.0` + +import ammonite.ops._ +// Ammonite script to encrypt private role keys for the key server database, +// needs the environment variables DB_ENCRYPTION_PASSWORD and DB_ENCRYPTION_SALT from key server, +// see https://confluence.in.here.com/pages/viewpage.action?pageId=972552231. + +import $ivy.`org.bouncycastle:bcprov-jdk15on:1.66` + +import java.security.Security +import java.util.Base64 +import javax.crypto.{Cipher, SecretKeyFactory} +import javax.crypto.spec.{PBEKeySpec, PBEParameterSpec} +import org.bouncycastle.jce.provider.BouncyCastleProvider +import scala.io.Source + +@main +def main(inputPath: String, inKey: String) = { + println(crypto.encrypt(Source.fromFile(secretKeyFilename).getLines.mkString)) +} From 888607f285e2c47eb64e7cda15e86092ee05c174 Mon Sep 17 00:00:00 2001 From: Simao Mata Date: Wed, 21 Jun 2023 15:54:41 +0200 Subject: [PATCH 3/6] Return 412 when keyserver returns 412 --- .../keyserver/KeyserverClient.scala | 5 ++- .../reposerver/http/RepoResourceSpec.scala | 14 ++++++++ scripts/insert-slow.sc | 36 ------------------- scripts/sign-role.sc | 23 ------------ 4 files changed, 18 insertions(+), 60 deletions(-) delete mode 100755 scripts/insert-slow.sc delete mode 100644 scripts/sign-role.sc diff --git a/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/keyserver/KeyserverClient.scala b/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/keyserver/KeyserverClient.scala index 7c6b2242..5e1ef74e 100644 --- a/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/keyserver/KeyserverClient.scala +++ b/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/keyserver/KeyserverClient.scala @@ -158,6 +158,9 @@ class KeyserverHttpClient(uri: Uri, httpClient: HttpRequest => Future[HttpRespon override def rotateRoot(repoId: RepoId): Future[Unit] = { val req = HttpRequest(HttpMethods.PUT, uri = apiUri(Path("root") / repoId.show / "rotate")) - execHttpUnmarshalled[Unit](req).ok + execHttpUnmarshalled[Unit](req).handleErrors { + case RemoteServiceError(_, StatusCodes.PreconditionFailed, _, _, _, _) => + Future.failed(RoleKeyNotFound) + } } } diff --git a/reposerver/src/test/scala/com/advancedtelematic/tuf/reposerver/http/RepoResourceSpec.scala b/reposerver/src/test/scala/com/advancedtelematic/tuf/reposerver/http/RepoResourceSpec.scala index 0c9a6092..5e7aacc1 100644 --- a/reposerver/src/test/scala/com/advancedtelematic/tuf/reposerver/http/RepoResourceSpec.scala +++ b/reposerver/src/test/scala/com/advancedtelematic/tuf/reposerver/http/RepoResourceSpec.scala @@ -1576,6 +1576,20 @@ class RepoResourceSpec extends TufReposerverSpec with RepoResourceSpecUtil } } + test("rotate returns 412 when key is offline") { + val repoId = RepoId.generate() + fakeKeyserverClient.createRoot(repoId).futureValue + + val root = fakeKeyserverClient.fetchRootRole(repoId).futureValue + + fakeKeyserverClient.deletePrivateKey(repoId, root.signed.roles(RoleType.ROOT).keyids.head).futureValue + + Put(apiUri(s"repo/${repoId.show}/root/rotate")) ~> routes ~> check { + status shouldBe StatusCodes.PreconditionFailed + } + } + + implicit class ErrorRepresentationOps(value: ErrorRepresentation) { def firstErrorCause: Option[String] = value.cause.flatMap(_.as[NonEmptyList[String]].toOption).map(_.head) diff --git a/scripts/insert-slow.sc b/scripts/insert-slow.sc deleted file mode 100755 index 52fbbe4e..00000000 --- a/scripts/insert-slow.sc +++ /dev/null @@ -1,36 +0,0 @@ -import $ivy.`org.mariadb.jdbc:mariadb-java-client:2.7.4` -import $ivy.`com.lihaoyi::ammonite-ops:2.4.0` - -import ammonite.ops._ - -import scala.concurrent.duration.Duration - -@main -def main(url: String, file: String) = { - val json: String = read! pwd/file - - import java.sql.Connection - import java.sql.DriverManager - val con = DriverManager.getConnection(url) - - val del = "delete from signed_roles where repo_id = '48c7372d-95aa-4dca-aafe-60b30a014dcc' limit 1 ;" - - con.createStatement().execute(del) - - val stm = con.prepareStatement("INSERT INTO signed_roles (repo_id, role_type, checksum, `length`, version, content) values (?, ?, ?, ?, ?,?)") - - stm.setString(1, "48c7372d-95aa-4dca-aafe-60b30a014dcc") - stm.setString(2, "TARGETS") - stm.setString(3, "x") - stm.setLong(4, 0) - stm.setLong(5, -1) - stm.setString(6, json) - - val startAt = System.nanoTime() - - val res = stm.executeUpdate() - - val took = Duration.fromNanos(System.nanoTime() - startAt) - - println(s"Finished: $res took ${took.toMillis}ms") -} diff --git a/scripts/sign-role.sc b/scripts/sign-role.sc deleted file mode 100644 index 356f00a7..00000000 --- a/scripts/sign-role.sc +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env amm - -import $ivy.`com.lihaoyi::ammonite-ops:2.4.0` -import $ivy.`io.github.uptane::libtuf:1.0.0` - -import ammonite.ops._ -// Ammonite script to encrypt private role keys for the key server database, -// needs the environment variables DB_ENCRYPTION_PASSWORD and DB_ENCRYPTION_SALT from key server, -// see https://confluence.in.here.com/pages/viewpage.action?pageId=972552231. - -import $ivy.`org.bouncycastle:bcprov-jdk15on:1.66` - -import java.security.Security -import java.util.Base64 -import javax.crypto.{Cipher, SecretKeyFactory} -import javax.crypto.spec.{PBEKeySpec, PBEParameterSpec} -import org.bouncycastle.jce.provider.BouncyCastleProvider -import scala.io.Source - -@main -def main(inputPath: String, inKey: String) = { - println(crypto.encrypt(Source.fromFile(secretKeyFilename).getLines.mkString)) -} From 4a4fbf12ad344fc801ca99a4a72ad08de68fe675 Mon Sep 17 00:00:00 2001 From: Ben Clouser Date: Wed, 28 Jun 2023 15:36:47 -0400 Subject: [PATCH 4/6] Add tuf-server back into project Signed-off-by: Ben Clouser --- tuf-server/build.sbt | 3 + .../src/main/resources/application.conf | 67 +++++++++++++++++++ tuf-server/src/main/resources/logback.xml | 9 +++ .../uptane/tuf/tuf_server/TufServerBoot.scala | 34 ++++++++++ 4 files changed, 113 insertions(+) create mode 100644 tuf-server/build.sbt create mode 100644 tuf-server/src/main/resources/application.conf create mode 100644 tuf-server/src/main/resources/logback.xml create mode 100644 tuf-server/src/main/scala/io/github/uptane/tuf/tuf_server/TufServerBoot.scala diff --git a/tuf-server/build.sbt b/tuf-server/build.sbt new file mode 100644 index 00000000..eb858dc7 --- /dev/null +++ b/tuf-server/build.sbt @@ -0,0 +1,3 @@ +Compile / mainClass := Some("io.github.uptane.tuf.tuf_server.TufServerBoot") + +// fork := true diff --git a/tuf-server/src/main/resources/application.conf b/tuf-server/src/main/resources/application.conf new file mode 100644 index 00000000..0645ea98 --- /dev/null +++ b/tuf-server/src/main/resources/application.conf @@ -0,0 +1,67 @@ +akka { + loggers = ["akka.event.slf4j.Slf4jLogger"] + loglevel = "DEBUG" + log-config-on-start = off + + http { + server { + max-connections = 1024 + backlog = 2000 + + // Akka HTTP default value + idle-timeout = 60 s + + // TODO: Might be a problem for some services used by the client (director, treehub?) + // turn off automatic HEAD->GET conversion, otherwise `head` routes get ignored + transparent-head-requests = false + } + } +} + +ats { + tuf-server { + db.default_host = "localhost" + http.default_client_host = "localhost" + } + + database { + migrate = true + asyncMigrations = true + skipMigrationCheck = true + + // TODO: Needs to be scoped to service? + encryption { + salt = "" + salt = ${?DB_ENCRYPTION_SALT} + password = "" + password = ${?DB_ENCRYPTION_PASSWORD} + } + } + + reposerver { + http { + server = { + host = "0.0.0.0" + port = 7100 + } + + client { + keyserver { + host = ${ats.tuf-server.http.default_client_host} + port = 7200 + } + } + } + database.url = "jdbc:mariadb://"${ats.tuf-server.db.default_host}":3306/tuf_repo" + } + + keyserver { + http.server { + host = "0.0.0.0" + port = 7200 + daemon-port = 9200 + } + + database.url = "jdbc:mariadb://"${ats.tuf-server.db.default_host}":3306/tuf_keyserver" + } +} diff --git a/tuf-server/src/main/resources/logback.xml b/tuf-server/src/main/resources/logback.xml new file mode 100644 index 00000000..23cb2e7c --- /dev/null +++ b/tuf-server/src/main/resources/logback.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tuf-server/src/main/scala/io/github/uptane/tuf/tuf_server/TufServerBoot.scala b/tuf-server/src/main/scala/io/github/uptane/tuf/tuf_server/TufServerBoot.scala new file mode 100644 index 00000000..623da13f --- /dev/null +++ b/tuf-server/src/main/scala/io/github/uptane/tuf/tuf_server/TufServerBoot.scala @@ -0,0 +1,34 @@ +package io.github.uptane.tuf.tuf_server + +import akka.actor.ActorSystem +import com.advancedtelematic.libats.boot.{VersionInfo, VersionInfoProvider} +import com.advancedtelematic.libats.http.BootAppDefaultConfig +import com.advancedtelematic.tuf.keyserver.KeyserverBoot +import com.advancedtelematic.tuf.keyserver.daemon.KeyserverDaemon +import com.advancedtelematic.tuf.reposerver.ReposerverBoot +import com.codahale.metrics.MetricRegistry +import com.typesafe.config.{Config, ConfigFactory} +import org.bouncycastle.jce.provider.BouncyCastleProvider + +import java.security.Security +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, Future} + +object TufServerBoot extends BootAppDefaultConfig with VersionInfo { + override protected lazy val provider: VersionInfoProvider = AppBuildInfo + + def main(args: Array[String]): Unit = { + Security.addProvider(new BouncyCastleProvider) + + val keyserverDbConfig = globalConfig.getConfig("ats.keyserver.database") + val keyserverActorSystem = ActorSystem("keyserver-actor-system") + val keyserverBind = new KeyserverBoot(globalConfig, keyserverDbConfig, new MetricRegistry)(keyserverActorSystem).bind() + + val reposerverDbConfig = globalConfig.getConfig("ats.reposerver.database") + val reposerverBind = new ReposerverBoot(globalConfig, reposerverDbConfig, new MetricRegistry)(ActorSystem("reposerver-actor-system")).bind() + + val bind = Future.sequence(List(keyserverBind, reposerverBind)) + + Await.result(bind, Duration.Inf) + } +} From c34bc253997526c20015f5b7b73cdafd657cd00f Mon Sep 17 00:00:00 2001 From: Ben Clouser Date: Tue, 20 Jun 2023 18:33:15 -0400 Subject: [PATCH 5/6] add delegation-role-refresh capability to reposerver client Signed-off-by: Ben Clouser --- .../libtuf_server/repo/client/ReposerverClient.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/repo/client/ReposerverClient.scala b/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/repo/client/ReposerverClient.scala index 5b66eb27..3460a695 100644 --- a/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/repo/client/ReposerverClient.scala +++ b/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/repo/client/ReposerverClient.scala @@ -26,7 +26,7 @@ import com.advancedtelematic.libtuf_server.repo.client.ReposerverClient.{KeysNot import io.circe.{Decoder, Encoder, Json} import com.advancedtelematic.libats.codecs.CirceCodecs._ import com.advancedtelematic.libtuf.data.ClientCodecs._ -import com.advancedtelematic.libtuf.data.ClientDataType.{ClientTargetItem, DelegationClientTargetItem, RootRole, TargetsRole} +import com.advancedtelematic.libtuf.data.ClientDataType.{ClientTargetItem, DelegatedRoleName, DelegationClientTargetItem, RootRole, TargetCustom, TargetsRole} import scala.concurrent.{ExecutionContext, Future} import scala.reflect.ClassTag @@ -116,6 +116,7 @@ trait ReposerverClient { def fetchDelegationMetadata(namespace: Namespace, roleName: String): Future[JsonSignedPayload] def fetchDelegationTargetItems(namespace: Namespace, nameContains: Option[String] = None): Future[PaginationResult[DelegationClientTargetItem]] def fetchSingleDelegationTargetItem(namespace: Namespace, targetFilename: TargetFilename): Future[Seq[DelegationClientTargetItem]] + def refreshDelegatedRole(namespace: Namespace, fileName: DelegatedRoleName): Future[Unit] } object ReposerverHttpClient extends ServiceHttpClientSupport { @@ -361,4 +362,10 @@ class ReposerverHttpClient(reposerverUri: Uri, httpClient: HttpRequest => Future execHttpUnmarshalledWithNamespace[Unit](namespace, req).handleErrors(addTargetErrorHandler) } } + + override def refreshDelegatedRole(namespace: Namespace, fileName: DelegatedRoleName): Future[Unit] = { + val uri = apiUri(Path("user_repo") / "trusted-delegations" / fileName.value / "remote"/ "refresh") + val req = HttpRequest(HttpMethods.PUT, uri) + execHttpUnmarshalledWithNamespace[Unit](namespace, req).ok + } } From 6d7ddfdb7fdc0cbedb9bce38a56b331eb79a9d95 Mon Sep 17 00:00:00 2001 From: Ben Clouser Date: Wed, 21 Jun 2023 19:23:53 -0400 Subject: [PATCH 6/6] [OTA-1879] Add client support for fetching trusted delegations and delegation info Signed-off-by: Ben Clouser --- .../repo/client/ReposerverClient.scala | 22 +++++++++++++++++-- .../libtuf/data/ClientCodecs.scala | 6 +++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/repo/client/ReposerverClient.scala b/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/repo/client/ReposerverClient.scala index 3460a695..c43ec20b 100644 --- a/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/repo/client/ReposerverClient.scala +++ b/libtuf-server/src/main/scala/com/advancedtelematic/libtuf_server/repo/client/ReposerverClient.scala @@ -22,11 +22,12 @@ import com.advancedtelematic.libtuf.data.TufCodecs._ import com.advancedtelematic.libtuf.data.TufDataType.TargetFormat.TargetFormat import com.advancedtelematic.libtuf.data.TufDataType.{HardwareIdentifier, JsonSignedPayload, KeyType, RepoId, SignedPayload, TargetFilename, TargetName, TargetVersion} import com.advancedtelematic.libtuf_server.data.Requests.{CommentRequest, CreateRepositoryRequest, FilenameComment, TargetComment} -import com.advancedtelematic.libtuf_server.repo.client.ReposerverClient.{KeysNotReady, NotFound, RootNotInKeyserver} +import com.advancedtelematic.libtuf_server.repo.client.ReposerverClient.{DelegationInfo, KeysNotReady, NotFound, RootNotInKeyserver} import io.circe.{Decoder, Encoder, Json} import com.advancedtelematic.libats.codecs.CirceCodecs._ +import com.advancedtelematic.libats.codecs.CirceValidatedGeneric.validatedGenericKeyDecoder import com.advancedtelematic.libtuf.data.ClientCodecs._ -import com.advancedtelematic.libtuf.data.ClientDataType.{ClientTargetItem, DelegatedRoleName, DelegationClientTargetItem, RootRole, TargetCustom, TargetsRole} +import com.advancedtelematic.libtuf.data.ClientDataType.{ClientTargetItem, DelegatedRoleName, Delegation, DelegationClientTargetItem, DelegationFriendlyName, RootRole, TargetCustom, TargetsRole} import scala.concurrent.{ExecutionContext, Future} import scala.reflect.ClassTag @@ -35,6 +36,7 @@ import io.circe.generic.semiauto._ import org.slf4j.LoggerFactory import java.net.URI +import java.time.Instant object ReposerverClient { @@ -57,7 +59,11 @@ object ReposerverClient { hardwareIds: Seq[HardwareIdentifier] = Seq.empty[HardwareIdentifier], proprietaryCustom: Option[Json] = None ) + case class DelegationInfo(lastFetched: Option[Instant], remoteUri: Option[Uri], friendlyName: Option[DelegationFriendlyName]=None) + object DelegationInfo { + implicit val delegationInfoCodec: io.circe.Codec[DelegationInfo] = io.circe.generic.semiauto.deriveCodec[DelegationInfo] + } val KeysNotReady = RawError(ErrorCode("keys_not_ready"), StatusCodes.Locked, "keys not ready") val RootNotInKeyserver = RawError(ErrorCode("root_role_not_in_keyserver"), StatusCodes.FailedDependency, "the root role was not found in upstream keyserver") val NotFound = RawError(ErrorCode("repo_resource_not_found"), StatusCodes.NotFound, "the requested repo resource was not found") @@ -116,6 +122,8 @@ trait ReposerverClient { def fetchDelegationMetadata(namespace: Namespace, roleName: String): Future[JsonSignedPayload] def fetchDelegationTargetItems(namespace: Namespace, nameContains: Option[String] = None): Future[PaginationResult[DelegationClientTargetItem]] def fetchSingleDelegationTargetItem(namespace: Namespace, targetFilename: TargetFilename): Future[Seq[DelegationClientTargetItem]] + def fetchTrustedDelegations(namespace: Namespace): Future[Delegation] + def fetchDelegationsInfo(namespace: Namespace): Future[Map[DelegatedRoleName, DelegationInfo]] def refreshDelegatedRole(namespace: Namespace, fileName: DelegatedRoleName): Future[Unit] } @@ -270,6 +278,16 @@ class ReposerverHttpClient(reposerverUri: Uri, httpClient: HttpRequest => Future execHttpUnmarshalledWithNamespace[PaginationResult[DelegationClientTargetItem]](namespace, req).ok } + override def fetchTrustedDelegations(namespace: Namespace): Future[Delegation] = { + val req = HttpRequest(HttpMethods.GET, uri=apiUri(Path("user_repo/trusted-delegations"))) + execHttpUnmarshalledWithNamespace[Delegation](namespace, req).ok + } + + override def fetchDelegationsInfo(namespace: Namespace): Future[Map[DelegatedRoleName, DelegationInfo]] = { + val req = HttpRequest(HttpMethods.GET, uri=apiUri(Path("user_repo/trusted-delegations/info"))) + execHttpUnmarshalledWithNamespace[Map[DelegatedRoleName, DelegationInfo]](namespace, req).ok + } + override def setTargetComments(namespace: Namespace, targetFilename: TargetFilename, comment: String): Future[Unit] = { val commentBody = HttpEntity(ContentTypes.`application/json`, CommentRequest(TargetComment(comment)).asJson.noSpaces) val req = HttpRequest(HttpMethods.PUT, uri = apiUri(Path(s"user_repo/comments/${targetFilename.value}")), entity = commentBody) diff --git a/libtuf/src/main/scala/com/advancedtelematic/libtuf/data/ClientCodecs.scala b/libtuf/src/main/scala/com/advancedtelematic/libtuf/data/ClientCodecs.scala index 71286d09..fe3d70cc 100644 --- a/libtuf/src/main/scala/com/advancedtelematic/libtuf/data/ClientCodecs.scala +++ b/libtuf/src/main/scala/com/advancedtelematic/libtuf/data/ClientCodecs.scala @@ -82,7 +82,13 @@ object ClientCodecs { implicit val delegatedRoleNameEncoder: Encoder[DelegatedRoleName] = ValidatedString.validatedStringEncoder implicit val delegatedRoleNameDecoder: Decoder[DelegatedRoleName] = ValidatedString.validatedStringDecoder + implicit val delegatedRoleNameKeyEncoder = new KeyEncoder[DelegatedRoleName] { + override def apply(roleName: DelegatedRoleName): String = roleName.value + } + implicit val delegatedRoleNameKeyDecoder = new KeyDecoder[DelegatedRoleName] { + override def apply(key: String): Option[DelegatedRoleName] = DelegatedRoleName.delegatedRoleNameValidation(key).toOption + } implicit val delegationFriendlyNameEncoder: Encoder[DelegationFriendlyName] = ValidatedString.validatedStringEncoder implicit val delegationFriendlyNameDecoder: Decoder[DelegationFriendlyName] = ValidatedString.validatedStringDecoder