Skip to content

Commit

Permalink
Options for setting the expiration date when signing
Browse files Browse the repository at this point in the history
Signed-off-by: Jochen Schneider <[email protected]>
  • Loading branch information
Jochen Schneider committed Aug 8, 2019
1 parent 89ec974 commit 6b7ac68
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 41 deletions.
29 changes: 21 additions & 8 deletions cli/src/main/scala/com/advancedtelematic/tuf/cli/Cli.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.advancedtelematic.tuf.cli
import java.net.URI
import java.nio.file.{Files, Path, Paths}
import java.security.Security
import java.time.Instant
import java.time.{Instant, Period}
import java.time.temporal.ChronoUnit

import cats.Eval
Expand Down Expand Up @@ -48,7 +48,8 @@ case class Config(command: Command,
keyIds: List[KeyId]= List.empty,
oldKeyId: Option[KeyId] = None,
version: Option[Int] = None,
expires: Instant = Instant.now().plus(1, ChronoUnit.DAYS),
expireOn: Option[Instant] = None,
expireAfter: Option[Period] = None,
length: Int = -1,
targetFilename: Option[TargetFilename] = None,
targetName: Option[TargetName] = None,
Expand Down Expand Up @@ -101,6 +102,16 @@ object Cli extends App with VersionInfo {
.text("Path where this executable will look for keys, by default it's the `user-keys` directory in `home-dir`")
}

lazy val expirationOpts: OptionParser[Config] => Seq[OptionDef[_, Config]] = { parser =>
Seq(
parser.opt[Instant]("expires")
.text("UTC instant such as '2018-01-01T00:01:00Z'")
.toConfigOptionParam('expireOn),
parser.opt[Period]("expire-after")
.text("Expiration delay in years, months and days (each optional, but in that order), such as '1Y3M5D'")
.toConfigOptionParam('expireAfter))
}

lazy val addTargetOptions: OptionParser[Config] => Seq[OptionDef[_, Config]] = { parser =>
Seq(
parser.opt[Int]("length")
Expand Down Expand Up @@ -288,9 +299,8 @@ object Cli extends App with VersionInfo {
),
cmd("sign")
.toCommand(SignRoot)
.children(
manyKeyNamesOpt(this)
)
.children(manyKeyNamesOpt(this))
.children(expirationOpts(this):_*)
)

cmd("targets")
Expand All @@ -305,8 +315,8 @@ object Cli extends App with VersionInfo {
.toConfigOptionParam('version)
.required(),
opt[Instant]("expires")
.toConfigParam('expires)
.text("UTC instant such as 2018-01-01T00:01:00Z")
.toConfigOptionParam('expireOn)
.text("UTC instant such as '2018-01-01T00:01:00Z'")
.required()
),
cmd("add")
Expand All @@ -330,7 +340,8 @@ object Cli extends App with VersionInfo {
opt[Int]("version")
.text("Ignore unsigned role version and use <version> instead")
.toConfigOptionParam('version)
),
)
.children(expirationOpts(this):_*),
cmd("pull")
.toCommand(PullTargets),
cmd("push")
Expand Down Expand Up @@ -399,6 +410,8 @@ object Cli extends App with VersionInfo {
c.command match {
case RemoveRootKey if c.keyIds.isEmpty && c.keyNames.isEmpty =>
"To remove a root key you need to specify at least one key id or key name".asLeft
case SignTargets | SignRoot if c.expireOn.isDefined && c.expireAfter.isDefined =>
"The expiration date should be given with either '--expires' or '--expire-after', not both".asLeft
case _ =>
Right(())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.advancedtelematic.tuf.cli
import com.advancedtelematic.libats.data.ErrorRepresentation
import com.advancedtelematic.libtuf.data.ErrorCodes
import com.advancedtelematic.libtuf.http.SHttpjServiceClient.HttpjClientError
import com.advancedtelematic.tuf.cli.Errors.CommandNotSupportedByRepositoryType
import com.advancedtelematic.tuf.cli.Errors.{CommandNotSupportedByRepositoryType, PastDate}
import com.advancedtelematic.tuf.cli.repo.TufRepo.TargetsPullError
import io.circe.syntax._
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -49,5 +49,8 @@ object CliHelp {

case CommandNotSupportedByRepositoryType(repoType, msg) =>
_log.error(s"The local repository is of type $repoType which does not support this command: $msg")

case PastDate() =>
_log.error("Given date lies in the past, use --force if you really want to use it")
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.advancedtelematic.tuf.cli

import java.nio.file.{Path, Paths}
import java.time.Instant
import java.time.{Instant, Period}

import com.advancedtelematic.libtuf.data.TufDataType.{Ed25519KeyType, KeyType, RsaKeyType, TargetFormat}
import eu.timepit.refined
Expand Down Expand Up @@ -80,6 +80,12 @@ object CliReads {

implicit val instantRead: Read[Instant] = Read.stringRead.map(Instant.parse)

// Period.addTo can only deal with days
implicit val periodRead: Read[Period] = Read.stringRead.map { s =>
val p = Period.parse(s"P$s")
Period.ofDays(365 * p.getYears + 30 * p.getMonths + p.getDays)
}

implicit val targetFormatRead: Read[TargetFormat] = Read.stringRead.map(_.toUpperCase).map(TargetFormat.withName)

implicit val repoServerTypeRead: Read[TufServerType] = Read.stringRead.map(_.toLowerCase()).map {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.advancedtelematic.tuf.cli

import java.net.URI
import java.time.{Instant, Period}
import java.time.temporal.ChronoUnit

import cats.implicits._
import com.advancedtelematic.libats.data.DataType.{HashMethod, ValidChecksum}
Expand All @@ -12,6 +14,7 @@ import com.advancedtelematic.libtuf.data.TufDataType.{HardwareIdentifier, Target
import com.advancedtelematic.libtuf.http.{ReposerverClient, TufServerClient}
import com.advancedtelematic.tuf.cli.CliConfigOptionOps._
import com.advancedtelematic.tuf.cli.Commands._
import com.advancedtelematic.tuf.cli.Errors.PastDate
import com.advancedtelematic.tuf.cli.TryToFuture._
import com.advancedtelematic.tuf.cli.repo._
import eu.timepit.refined._
Expand All @@ -24,6 +27,10 @@ import scala.language.implicitConversions
import scala.util.Try

object CommandHandler {

val DEFAULT_ROOT_LIFETIME: Period = Period.ofDays(365)
val DEFAULT_TARGET_LIFETIME: Period = Period.ofDays(31)

private lazy val log = LoggerFactory.getLogger(this.getClass)

private implicit def tryToFutureConversion[T](value: Try[T]): Future[T] = value.toFuture
Expand All @@ -39,6 +46,14 @@ object CommandHandler {
}
} yield targetFilename -> newTarget

def expirationDate(config: Config, fallback: Period, now: Instant = Instant.now()): Instant = {
config.expireAfter
.map(now.plus(_))
.orElse(config.expireOn)
.map{ d => if (d.isBefore(now) && !config.force) throw PastDate(); d }
.getOrElse(now.plus(fallback))
}

def handle[S <: TufServerClient](tufRepo: => TufRepo[S],
repoServer: => Future[S],
delegationsServer: => Future[ReposerverClient],
Expand All @@ -60,7 +75,8 @@ object CommandHandler {
config.rootKey,
config.oldRootKey,
config.oldKeyId,
config.keyNames.headOption)
config.keyNames.headOption,
Instant.now().plus(DEFAULT_ROOT_LIFETIME))
.map(_ => log.info(s"root keys moved offline, root.json saved to ${tufRepo.repoPath}"))
}

Expand All @@ -71,7 +87,7 @@ object CommandHandler {

case InitTargets =>
tufRepo
.initTargets(config.version.valueOrConfigError, config.expires)
.initTargets(config.version.valueOrConfigError, config.expireOn.getOrElse(Instant.now().plus(DEFAULT_TARGET_LIFETIME)))
.map(p => log.info(s"Wrote empty targets to $p"))


Expand Down Expand Up @@ -99,7 +115,7 @@ object CommandHandler {

case SignTargets =>
tufRepo
.signTargets(config.keyNames, config.version)
.signTargets(config.keyNames, expirationDate(config, DEFAULT_TARGET_LIFETIME), config.version)
.map(p => log.info(s"signed targets.json to $p"))


Expand Down Expand Up @@ -130,7 +146,7 @@ object CommandHandler {
.map(_ => log.info("Pushed root.json"))

case SignRoot =>
tufRepo.signRoot(config.keyNames)
tufRepo.signRoot(config.keyNames, expirationDate(config, DEFAULT_ROOT_LIFETIME))
.map(p => log.info(s"signed root.json saved to $p"))

case AddRootKey =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ object Errors {
case class CliArgumentMissing(msg: String) extends Exception(msg)
case class CommandNotSupportedByRepositoryType(repoType: TufServerType, msg: String) extends Exception(msg) with NoStackTrace
case class DelegationsAlreadySigned(path: Path) extends Exception(s"Delegations file $path is already signed, convert it to an unsigned file or provide an unsigned file")
case class PastDate() extends Exception
}
35 changes: 18 additions & 17 deletions cli/src/main/scala/com/advancedtelematic/tuf/cli/repo/TufRepo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,6 @@ abstract class TufRepo[S <: TufServerClient](val repoPath: Path)(implicit ec: Ex

private lazy val rolesPath = repoPath.resolve("roles")

protected val DEFAULT_ROOT_EXPIRE_TIME = Period.ofDays(365)
private val DEFAULT_TARGET_EXPIRE_TIME = Period.ofDays(31)

def initRepoDirs(): Try[Unit] = Try {
val perms = PosixFilePermissions.asFileAttribute(java.util.EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE))

Expand Down Expand Up @@ -202,13 +199,13 @@ abstract class TufRepo[S <: TufServerClient](val repoPath: Path)(implicit ec: Ex
_ <- client.pushSignedRoot(signedRoot)
} yield ()

def signRoot(keys: Seq[KeyName], version: Option[Int] = None): Try[Path] =
signRole[RootRole](version, keys)
def signRoot(keys: Seq[KeyName], expiration: Instant, version: Option[Int] = None): Try[Path] =
signRole[RootRole](version, keys, expiration)

import cats.implicits._

protected def signRole[T : Decoder : Encoder](version: Option[Int], keys: Seq[KeyName])
(implicit tufRole: TufRole[T]): Try[Path] = {
protected def signRole[T : Decoder : Encoder](version: Option[Int], keys: Seq[KeyName], expiration: Instant)
(implicit tufRole: TufRole[T]): Try[Path] = {
def signatures(payload: T): Try[List[ClientSignature]] =
keys.toList.traverse { key =>
keyStorage.readKeyPair(key).map { case (pub, priv) =>
Expand All @@ -218,7 +215,7 @@ abstract class TufRepo[S <: TufServerClient](val repoPath: Path)(implicit ec: Ex

for {
unsigned <- readUnsignedRole[T]
newUnsigned = tufRole.refreshRole(unsigned, oldV => version.getOrElse(oldV + 1), Instant.now().plus(DEFAULT_TARGET_EXPIRE_TIME))
newUnsigned = tufRole.refreshRole(unsigned, oldV => version.getOrElse(oldV + 1), expiration)
sigs <- signatures(newUnsigned)
signedRole = SignedPayload(sigs, newUnsigned, newUnsigned.asJson)
path <- writeSignedRole(signedRole)
Expand Down Expand Up @@ -309,7 +306,8 @@ abstract class TufRepo[S <: TufServerClient](val repoPath: Path)(implicit ec: Ex
newRootName: KeyName,
oldRootName: KeyName,
oldKeyId: Option[KeyId],
newTargetsName: Option[KeyName]): Future[SignedPayload[RootRole]]
newTargetsName: Option[KeyName],
rootExpireTime: Instant): Future[SignedPayload[RootRole]]

def initTargets(version: Int, expires: Instant): Try[Path]

Expand All @@ -320,7 +318,7 @@ abstract class TufRepo[S <: TufServerClient](val repoPath: Path)(implicit ec: Ex
def addTargetDelegation(name: DelegatedRoleName, key: List[TufKey],
delegatedPaths: List[DelegatedPathPattern], threshold: Int): Try[Path]

def signTargets(targetsKeys: Seq[KeyName], version: Option[Int] = None): Try[Path]
def signTargets(targetsKeys: Seq[KeyName], expiration: Instant, version: Option[Int] = None): Try[Path]

def pullVerifyTargets(reposerverClient: S, rootRole: RootRole): Future[SignedPayload[TargetsRole]]

Expand Down Expand Up @@ -388,14 +386,16 @@ class RepoServerRepo(repoPath: Path)(implicit ec: ExecutionContext) extends TufR
} yield path
}

override def signTargets(targetsKeys: Seq[KeyName], version: Option[Int] = None): Try[Path] =
signRole[TargetsRole](version, targetsKeys)
// TODO
override def signTargets(targetsKeys: Seq[KeyName], expiration: Instant, version: Option[Int] = None): Try[Path] =
signRole[TargetsRole](version, targetsKeys, expiration)

override def moveRootOffline(repoClient: ReposerverClient,
newRootName: KeyName,
oldRootName: KeyName,
oldKeyId: Option[KeyId],
newTargetsName: Option[KeyName]): Future[SignedPayload[RootRole]] = {
newTargetsName: Option[KeyName],
rootExpireTime: Instant): Future[SignedPayload[RootRole]] = {
assert(newTargetsName.isDefined, "new targets key name must be defined when moving root off line in tuf-reposerver")

for {
Expand All @@ -411,7 +411,7 @@ class RepoServerRepo(repoPath: Path)(implicit ec: ExecutionContext) extends TufR
.withRoleKeys(RoleType.ROOT, threshold = 1, newRootPubKey)
.withRoleKeys(RoleType.TARGETS, threshold = 1, newTargetsPubKey)
.withVersion(oldRootRole.version + 1)
.addExpires(DEFAULT_ROOT_EXPIRE_TIME)
.copy(expires = rootExpireTime)
newRootSignature = TufCrypto.signPayload(newRootPrivKey, newRootRole.asJson).toClient(newRootPubKey.id)
newRootOldSignature = TufCrypto.signPayload(oldRootPrivKey, newRootRole.asJson).toClient(oldRootPubKeyId)
newSignedRoot = SignedPayload(Seq(newRootSignature, newRootOldSignature), newRootRole, newRootRole.asJson)
Expand Down Expand Up @@ -460,7 +460,8 @@ class DirectorRepo(repoPath: Path)(implicit ec: ExecutionContext) extends TufRep
newRootName: KeyName,
oldRootName: KeyName,
oldKeyId: Option[KeyId],
newTargetsName: Option[KeyName]): Future[SignedPayload[RootRole]] = {
newTargetsName: Option[KeyName],
rootExpireTime: Instant): Future[SignedPayload[RootRole]] = {
assert(newTargetsName.isEmpty, "new targets key name must be empty for director")

for {
Expand All @@ -474,7 +475,7 @@ class DirectorRepo(repoPath: Path)(implicit ec: ExecutionContext) extends TufRep
newRootRole = oldRootRole
.withRoleKeys(RoleType.ROOT, threshold = 1, newRootPubKey)
.withVersion(oldRootRole.version + 1)
.addExpires(DEFAULT_ROOT_EXPIRE_TIME)
.copy(expires = rootExpireTime)
newRootSignature = TufCrypto.signPayload(newRootPrivKey, newRootRole.asJson).toClient(newRootPubKey.id)
newRootOldSignature = TufCrypto.signPayload(oldRootPrivKey, newRootRole.asJson).toClient(oldRootPubKeyId)
newSignedRoot = SignedPayload(Seq(newRootSignature, newRootOldSignature), newRootRole, newRootRole.asJson)
Expand All @@ -493,7 +494,7 @@ class DirectorRepo(repoPath: Path)(implicit ec: ExecutionContext) extends TufRep
override def deleteTarget(filename: TargetFilename): Try[Path] =
Failure(CommandNotSupportedByRepositoryType(Director, "deleteTarget"))

override def signTargets(targetsKeys: Seq[KeyName], version: Option[Int]): Try[Path] =
override def signTargets(targetsKeys: Seq[KeyName], expiration: Instant, version: Option[Int]): Try[Path] =
Failure(CommandNotSupportedByRepositoryType(Director, "signTargets"))

override def pullVerifyTargets(client: DirectorClient, rootRole: RootRole): Future[SignedPayload[TargetsRole]] =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.advancedtelematic.tuf.cli

import java.time.{Instant, Period}

import com.advancedtelematic.tuf.cli.Errors.PastDate
import org.scalatest.{FunSuite, Matchers}

class CommandHandlerExpirationDateSpec extends FunSuite with Matchers {
val now = Instant.EPOCH
val oneDay = Period.ofDays(1)
val inADay = now.plus(oneDay)

// the actual command doesn't matter
private val cmd = Commands.SignRoot

test("expirationDate from fallback") {
val d = CommandHandler.expirationDate(Config(cmd), oneDay, now)
d shouldBe inADay
}

test("expirationDate from expireOn") {
val d = CommandHandler.expirationDate(Config(cmd, expireOn = Some(inADay)), Period.ZERO, now)
d shouldBe inADay
}

test("expirationDate from expireOn in the past") {
intercept[PastDate] {
CommandHandler.expirationDate(Config(cmd, expireOn = Some(now.minusSeconds(1))), Period.ZERO, now)
}
}

test("expirationDate from expireOn in the past, but forced") {
val d = CommandHandler.expirationDate(Config(cmd, force = true, expireOn = Some(now.minusSeconds(1))),
Period.ZERO, now)
d shouldBe now.minusSeconds(1)
}

test("expirationDate from expireAfter") {
val d = CommandHandler.expirationDate(Config(cmd, expireAfter = Some(oneDay)), Period.ZERO, now)
d shouldBe inADay
}

test("expirationDate from expireAfter in the past") {
intercept[PastDate] {
CommandHandler.expirationDate(Config(cmd, expireAfter = Some(Period.ofDays(-1))), Period.ZERO, now)
}
}

test("expirationDate from expireAfter in the past, but forced") {
val d = CommandHandler.expirationDate(Config(cmd, force = true, expireAfter = Some(Period.ofDays(-1))),
Period.ZERO, now)
d shouldBe now.minus(oneDay)
}

}
Loading

0 comments on commit 6b7ac68

Please sign in to comment.