From 358777fad2afbf7a7f3719367e1f4b0d73c2a42e Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Tue, 10 Apr 2018 21:37:07 -0700 Subject: Initial commit --- .gitignore | 1 + README.md | 72 +++++++++++++++++++++++++++++++++++++++++++++ build.sbt | 2 ++ project/build.properties | 1 + project/plugins.sbt | 1 + publish.sbt | 25 ++++++++++++++++ src/main/scala/Gpg.scala | 36 +++++++++++++++++++++++ src/main/scala/SbtGpg.scala | 58 ++++++++++++++++++++++++++++++++++++ 8 files changed, 196 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.sbt create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 publish.sbt create mode 100644 src/main/scala/Gpg.scala create mode 100644 src/main/scala/SbtGpg.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f97022 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e80c50 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# sbt-gpg + +Simple and secure artifact signing for sbt. + +This sbt plugin aims to make artifact signing simple and +unobtrusive. It is guided by two core ideas: + +1. easy configuration with sane defaults +2. use of standard cryptography tools (gpg) + +The motivation is that these priniciple are both essential in +promoting secure builds. + +## Highlights + +- Uses the system command `gpg` to do all operations. *This enables + advanced features such as use of smartcards or cutting-edge + ciphers.* + +- Hooks into the `publish` and `publishLocal` tasks. *All artrifacts + will be signed; there is no need to run a separate `publishSigned` + task.* + +- Unobtrusive configuration. *Key selection can be done through sbt's + `credentials` mechanism, thus enabling global configuration without + the need of adding a global plugin.* + +- Works out-of-the-box. *Publishing falls back to unsigned artifacts + in case key material cannot be found, after emitting an explicit + warning.* + +## Requirements + +- sbt version >= 1.0.0 +- gpg installed on user's machine (this requirement won't get in the + way of a user's productivity; missing gpg will simply disable the + functionality provided by this plugin) + +## Getting started +```scala +addSbtPlugin("io.crashbox" % "sbt-gpg" % "") +``` +Copy the above snippet to an sbt configuration file. E.g. + +- `project/plugins.sbt` to enable the plugin on a per-project basis +- `~/.sbt/1.0/plugins/gpg.sbt` to enable the plugin globally + +The autoplugin "SbtGpg" will be enabled and modify the `publish` and +`publishLocal` tasks to include signatures of all published artifacts. + +## Configuration + +### Signing key +By default, all signing operations will use `gpg`'s default key. A +specific key can be used by setting sbt `Credentials` for the host +"gpg". + +```scala +credentials += Credentials( + "GnuPG Key ID", + "gpg", + "4E7DA7B5A0F86992D6EB3F514601878662E33372", + "ignored" +) +``` + +The user name (3rd field) will determine the key to use and can be any +valid key id, fingerprint, email or user accepted by gpg. + +### Other settings +Check out the [autoplugin definition](src/main/scala/SbtGpg.scala) for +an exhaustive list of settings and tasks that can be customized. diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..d61b82e --- /dev/null +++ b/build.sbt @@ -0,0 +1,2 @@ +sbtPlugin := true +name := "sbt-gpg" diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..40577b0 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.1.2 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..f2c990e --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.4.0") diff --git a/publish.sbt b/publish.sbt new file mode 100644 index 0000000..46c1c49 --- /dev/null +++ b/publish.sbt @@ -0,0 +1,25 @@ +organization in ThisBuild := "io.crashbox" +licenses in ThisBuild := Seq( + ("Apache 2.0", url("https://www.apache.org/licenses/LICENSE-2.0"))) +homepage in ThisBuild := Some(url("https://github.com/jodersky/sbt-gpg")) +publishMavenStyle in ThisBuild := true +publishTo in ThisBuild := Some( + if (isSnapshot.value) + Opts.resolver.sonatypeSnapshots + else + Opts.resolver.sonatypeStaging +) +scmInfo in ThisBuild := Some( + ScmInfo( + url("https://github.com/jodersky/sbt-gpg"), + "scm:git@github.com:jodersky/sbt-gpg.git" + ) +) +developers in ThisBuild := List( + Developer( + id = "jodersky", + name = "Jakob Odersky", + email = "jakob@odersky.com", + url = url("https://crashbox.io") + ) +) diff --git a/src/main/scala/Gpg.scala b/src/main/scala/Gpg.scala new file mode 100644 index 0000000..cde4dba --- /dev/null +++ b/src/main/scala/Gpg.scala @@ -0,0 +1,36 @@ +package io.crashbox.gpg + +import java.io.File + +import scala.util.control.NonFatal +import sys.process._ + +class Gpg( + command: String, + options: Seq[String] = Seq.empty, + keyId: Option[String] = None)(log: String => Unit = System.err.println) { + + def run(params: String*): Int = + try { + val idOption = keyId.toSeq.flatMap(id => Seq("--local-user", id)) + val process = Process(command, options ++ idOption ++ params).run() + process.exitValue() + } catch { + case NonFatal(ex) => + log(ex.getMessage) + 127 + } + + def sign(file: File): Option[File] = { + val out = new File(file.getAbsolutePath + ".asc") + run("--armor", + "--output", + out.getAbsolutePath, + "--detach-sign", + file.getAbsolutePath) match { + case 0 => Some(out) + case _ => None + } + } + +} diff --git a/src/main/scala/SbtGpg.scala b/src/main/scala/SbtGpg.scala new file mode 100644 index 0000000..a015677 --- /dev/null +++ b/src/main/scala/SbtGpg.scala @@ -0,0 +1,58 @@ +package io.crashbox.gpg + +import sbt.{AutoPlugin, Def, _} +import sbt.Keys._ +import sbt.plugins.JvmPlugin + +object SbtGpg extends AutoPlugin { + + override def requires = JvmPlugin + override def trigger = allRequirements + + object autoImport { + val gpgCommand = settingKey[String]("Path to GnuPG executable.") + val gpgOptions = + settingKey[Seq[String]]("Additional global options to pass to gpg.") + val gpgKey = taskKey[Option[String]]( + "Key ID used to sign artifacts. Setting this to None will " + + "cause sbt-gpg to fall back to using gpg's default key. When set, " + + "it is equivalent to gpg's `--local-user` option.") + val gpg = + taskKey[Gpg]("Utility wrapper to the underlying gpg executable.") + } + import autoImport._ + + lazy val gpgSettings: Seq[Setting[_]] = Seq( + gpgCommand := "gpg", + gpgOptions := Seq("--yes"), + gpgKey := Credentials.forHost(credentials.value, "gpg").map(_.userName), + gpg := { + val log = streams.value.log + new Gpg(gpgCommand.value, gpgOptions.value, gpgKey.value)(log.warn(_)) + } + ) + + lazy val signingSettings: Seq[Setting[_]] = Seq( + packagedArtifacts := { + val log = streams.value.log + val arts: Map[Artifact, File] = packagedArtifacts.value + var failed = false + arts.map { + case (art, file) if !failed => + gpg.value.sign(file) match { + case Some(signed) => + art.withExtension(art.extension + ".asc") -> signed + case None => + log.warn("GPG reported an error. Artifacts won't be signed.") + failed = true + art -> file + } + case (art, file) => art -> file + } + } + ) + + override lazy val projectSettings + : Seq[Def.Setting[_]] = gpgSettings ++ signingSettings + +} -- cgit v1.2.3