aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob Odersky <jakob@odersky.com>2018-04-10 21:37:07 -0700
committerJakob Odersky <jakob@odersky.com>2018-04-10 21:37:07 -0700
commit358777fad2afbf7a7f3719367e1f4b0d73c2a42e (patch)
tree0d1d7a64a1b180b8e9beb87e15b12b111b7fc061
downloadsbt-gpg-358777fad2afbf7a7f3719367e1f4b0d73c2a42e.tar.gz
sbt-gpg-358777fad2afbf7a7f3719367e1f4b0d73c2a42e.tar.bz2
sbt-gpg-358777fad2afbf7a7f3719367e1f4b0d73c2a42e.zip
Initial commit
-rw-r--r--.gitignore1
-rw-r--r--README.md72
-rw-r--r--build.sbt2
-rw-r--r--project/build.properties1
-rw-r--r--project/plugins.sbt1
-rw-r--r--publish.sbt25
-rw-r--r--src/main/scala/Gpg.scala36
-rw-r--r--src/main/scala/SbtGpg.scala58
8 files changed, 196 insertions, 0 deletions
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" % "<latest_tag>")
+```
+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
+
+}