From 77266cd54635d8d19342937a8c47544162a6f763 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Sun, 12 Aug 2018 15:14:53 -0700 Subject: [PATCH 1/6] Use pinentry --- .gitignore | 1 + .../scala/com/typesafe/sbt/pgp/PgpKeys.scala | 1 + .../com/typesafe/sbt/pgp/PgpSettings.scala | 9 +++++- .../com/typesafe/sbt/pgp/PgpSigner.scala | 28 +++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 75894ea..683d0c3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ boot/ .classpath .project .scala_dependencies +.idea/ diff --git a/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpKeys.scala b/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpKeys.scala index 12713c1..e827f82 100644 --- a/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpKeys.scala +++ b/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpKeys.scala @@ -28,6 +28,7 @@ object PgpKeys { val gpgCommand = SettingKey[String]("gpg-command", "The path of the GPG command to run", BSetting) val useGpg = SettingKey[Boolean]("use-gpg", "If this is set to true, the GPG command line will be used.", ASetting) val useGpgAgent = SettingKey[Boolean]("use-gpg-agent", "If this is set to true, the GPG command line will expect a GPG agent for the password.", BSetting) + val useGpgPinentry = SettingKey[Boolean]("use-gpg-pinentry", "If this is set to true, the GPG command line will expect pinentry will be used with gpg-agent.", ASetting) val gpgAncient = SettingKey[Boolean]("gpg-ancient","Set this to true if you use a gpg version older than 2.1.") // Checking PGP Signatures options diff --git a/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSettings.scala b/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSettings.scala index e6dbd5d..96bacb2 100644 --- a/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSettings.scala +++ b/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSettings.scala @@ -19,6 +19,7 @@ object PgpSettings { // TODO - DO these belong lower? def useGpg = PgpKeys.useGpg in Global def useGpgAgent = PgpKeys.useGpgAgent in Global + def useGpgPinentry = PgpKeys.useGpgPinentry in Global def pgpSigningKey = PgpKeys.pgpSigningKey in Global def pgpPassphrase = PgpKeys.pgpPassphrase in Global def pgpReadOnly = PgpKeys.pgpReadOnly in Global @@ -29,6 +30,7 @@ object PgpSettings { lazy val gpgConfigurationSettings: Seq[Setting[_]] = Seq( PgpKeys.useGpg := false, PgpKeys.useGpgAgent := false, + PgpKeys.useGpgPinentry := false, PgpKeys.gpgCommand := (if(isWindows) "gpg.exe" else "gpg") ) @@ -117,6 +119,11 @@ object PgpSettings { new CommandLineGpgSigner(gpgCommand.value, useGpgAgent.value, pgpSecretRing.value.getPath, pgpSigningKey.value, pgpPassphrase.value) } + /** Helper to initialize the GPG PgpSigner with Pinentry */ + private[this] def gpgPinEntrySigner: Def.Initialize[Task[PgpSigner]] = Def.task { + new CommandLineGpgPinEntrySigner(gpgCommand.value, useGpgAgent.value, pgpSigningKey.value, pgpPassphrase.value) + } + /** Helper to initialize the BC PgpVerifier */ private[this] def bcPgpVerifierFactory: Def.Initialize[Task[PgpVerifierFactory]] = Def.task { new BouncyCastlePgpVerifierFactory(pgpCmdContext.value) @@ -134,7 +141,7 @@ object PgpSettings { lazy val signVerifyConfigurationSettings: Seq[Setting[_]] = Seq( // TODO - move these to the signArtifactSettings? skip in pgpSigner := ((skip in pgpSigner) ?? false).value, - pgpSigner := switch(useGpg, gpgSigner, bcPgpSigner).value, + pgpSigner := switch(useGpg, switch(useGpgPinentry, gpgPinEntrySigner, gpgSigner), bcPgpSigner).value, pgpVerifierFactory := switch(useGpg, gpgVerifierFactory, bcPgpVerifierFactory).value ) diff --git a/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSigner.scala b/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSigner.scala index 8be7282..d5b7078 100644 --- a/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSigner.scala +++ b/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSigner.scala @@ -32,6 +32,34 @@ class CommandLineGpgSigner(command: String, agent: Boolean, secRing: String, opt override val toString: String = "GPG-Command(" + command + ")" } + +/** + * A GpgSigner that uses the command-line to run gpg with a GPG smartcard. + * + * Yubikey 4 has OpenPGP support: https://developers.yubico.com/PGP/ so we can call + * it directly, and the secret key resides on the card. This means we need pinentry + * to be used, and there is no secret key ring. + */ +class CommandLineGpgPinEntrySigner(command: String, agent: Boolean, optKey: Option[Long], optPassphrase: Option[Array[Char]]) extends PgpSigner { + def sign(file: File, signatureFile: File, s: TaskStreams): File = { + if (signatureFile.exists) IO.delete(signatureFile) + // (the PIN code is the passphrase) + // https://wiki.archlinux.org/index.php/GnuPG#Unattended_passphrase + val pinentryargs: Seq[String] = Seq("--pinentry-mode", "loopback") + val passargs: Seq[String] = (optPassphrase map { passArray => passArray mkString "" } map { pass => Seq("--passphrase", pass) }) getOrElse Seq.empty + val keyargs: Seq[String] = optKey map (k => Seq("--default-key", "0x%x" format(k))) getOrElse Seq.empty + val args = passargs ++ pinentryargs ++ Seq("--detach-sign", "--armor") ++ (if(agent) Seq("--use-agent") else Seq.empty) ++ keyargs + val allArguments: Seq[String] = args ++ Seq("--output", signatureFile.getAbsolutePath, file.getAbsolutePath) + sys.process.Process(command, allArguments) ! s.log match { + case 0 => () + case n => sys.error(s"Failure running '${command + " " + allArguments.mkString(" ")}'. Exit code: " + n) + } + signatureFile + } + + override val toString: String = "GPG-Agent-Command(" + command + ")" +} + /** A GpgSigner that uses bouncy castle. */ class BouncyCastlePgpSigner(ctx: PgpCommandContext, optKey: Option[Long]) extends PgpSigner { import ctx.{secretKeyRing => secring, withPassphrase} From e0003f1af3d253ce934a787c8d876f554260cfed Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Sun, 12 Aug 2018 19:39:23 -0700 Subject: [PATCH 2/6] Update documentation --- src/jekyll/usage.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/jekyll/usage.md b/src/jekyll/usage.md index de186ce..7bc1439 100644 --- a/src/jekyll/usage.md +++ b/src/jekyll/usage.md @@ -25,6 +25,15 @@ By default `sbt-pgp` will use the default private keys from the standard gpg key There is currently no way to choose a non-default key from the keyring. +### OpenPGP Support ### + +If you are using a [Yubikey 4](https://github.com/drduh/YubiKey-Guide) or another smartcard that [supports OpenPGP](https://github.com/open-keychain/open-keychain/wiki/Security-Tokens), then you may have private keys implemented directly on the smartcard rather than using the gpg keyring. In this situation, you will probably use `gpg-agent` and a pinentry (`pinentry-mac`, `pinentry-qt`, `pinentry-curses` etc) rather than a passphrase. Set `useGpgPinentry := true` in your `build.sbt` settings to configure `sbt-pgp` appropriately. + + useGpgAgent := true + useGpgPinentry := true + +Note that `sbt-pgp` only supports OpenPGP through the GPG command line tool, and does not implement [Javacard support](https://incenp.org/notes/2016/openpgp-card-implementations.html). + # Creating a Key Pair # To create a key pair, enter the following: From 7bf24718261e17b6ca8ba7161fbc67666f9932d8 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Sun, 12 Aug 2018 19:50:20 -0700 Subject: [PATCH 3/6] Rename pinentry to not have caps --- .../src/main/scala/com/typesafe/sbt/pgp/PgpSettings.scala | 2 +- pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSigner.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSettings.scala b/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSettings.scala index 96bacb2..9c44a58 100644 --- a/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSettings.scala +++ b/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSettings.scala @@ -121,7 +121,7 @@ object PgpSettings { /** Helper to initialize the GPG PgpSigner with Pinentry */ private[this] def gpgPinEntrySigner: Def.Initialize[Task[PgpSigner]] = Def.task { - new CommandLineGpgPinEntrySigner(gpgCommand.value, useGpgAgent.value, pgpSigningKey.value, pgpPassphrase.value) + new CommandLineGpgPinentrySigner(gpgCommand.value, useGpgAgent.value, pgpSigningKey.value, pgpPassphrase.value) } /** Helper to initialize the BC PgpVerifier */ diff --git a/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSigner.scala b/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSigner.scala index d5b7078..fae3e55 100644 --- a/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSigner.scala +++ b/pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSigner.scala @@ -40,7 +40,7 @@ class CommandLineGpgSigner(command: String, agent: Boolean, secRing: String, opt * it directly, and the secret key resides on the card. This means we need pinentry * to be used, and there is no secret key ring. */ -class CommandLineGpgPinEntrySigner(command: String, agent: Boolean, optKey: Option[Long], optPassphrase: Option[Array[Char]]) extends PgpSigner { +class CommandLineGpgPinentrySigner(command: String, agent: Boolean, optKey: Option[Long], optPassphrase: Option[Array[Char]]) extends PgpSigner { def sign(file: File, signatureFile: File, s: TaskStreams): File = { if (signatureFile.exists) IO.delete(signatureFile) // (the PIN code is the passphrase) From c14c8dba175f8ec299663bc85a88602947584a8c Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Sun, 12 Aug 2018 20:53:22 -0700 Subject: [PATCH 4/6] Update with exposed key --- pgp-plugin/src/main/scala/com/typesafe/sbt/SbtPgp.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/pgp-plugin/src/main/scala/com/typesafe/sbt/SbtPgp.scala b/pgp-plugin/src/main/scala/com/typesafe/sbt/SbtPgp.scala index c5d41c2..433f146 100644 --- a/pgp-plugin/src/main/scala/com/typesafe/sbt/SbtPgp.scala +++ b/pgp-plugin/src/main/scala/com/typesafe/sbt/SbtPgp.scala @@ -23,6 +23,7 @@ object SbtPgp extends AutoPlugin { // TODO - Are these ok for style guide? We think so. def useGpg = PgpKeys.useGpg in Global def useGpgAgent = PgpKeys.useGpgAgent in Global + def useGpgPinentry = PgpKeys.useGpgPinentry in Global def pgpSigningKey = PgpKeys.pgpSigningKey in Global def pgpPassphrase = PgpKeys.pgpPassphrase in Global def pgpReadOnly = PgpKeys.pgpReadOnly in Global From 4e5bd389381e40a2cd83a33de9d64ddeec38109b Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Mon, 13 Aug 2018 07:59:54 -0700 Subject: [PATCH 5/6] fix links in documentation to be clearer --- .java-version | 1 - src/jekyll/usage.md | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 .java-version diff --git a/.java-version b/.java-version deleted file mode 100644 index d3bdbdf..0000000 --- a/.java-version +++ /dev/null @@ -1 +0,0 @@ -1.7 diff --git a/src/jekyll/usage.md b/src/jekyll/usage.md index 7bc1439..e9c9830 100644 --- a/src/jekyll/usage.md +++ b/src/jekyll/usage.md @@ -27,12 +27,12 @@ There is currently no way to choose a non-default key from the keyring. ### OpenPGP Support ### -If you are using a [Yubikey 4](https://github.com/drduh/YubiKey-Guide) or another smartcard that [supports OpenPGP](https://github.com/open-keychain/open-keychain/wiki/Security-Tokens), then you may have private keys implemented directly on the smartcard rather than using the gpg keyring. In this situation, you will probably use `gpg-agent` and a pinentry (`pinentry-mac`, `pinentry-qt`, `pinentry-curses` etc) rather than a passphrase. Set `useGpgPinentry := true` in your `build.sbt` settings to configure `sbt-pgp` appropriately. +If you are using a [Yubikey 4](https://www.yubico.com/product/yubikey-4-series/) or another smartcard that [supports OpenPGP](https://incenp.org/notes/2016/openpgp-card-implementations.html), then you may have private keys implemented directly on the smartcard rather than using the gpg keyring. In this situation, you will use `gpg-agent` and a pinentry (`pinentry-mac`, `pinentry-qt`, `pinentry-curses` etc) rather than a passphrase. Set `useGpgPinentry := true` in your `build.sbt` settings to configure `sbt-pgp` appropriately. useGpgAgent := true useGpgPinentry := true -Note that `sbt-pgp` only supports OpenPGP through the GPG command line tool, and does not implement [Javacard support](https://incenp.org/notes/2016/openpgp-card-implementations.html). +Note that `sbt-pgp` only supports OpenPGP through the GPG command line tool -- it is not available through bouncycastle. In addition, you may need to explicitly [enable support for OpenPGP on the Yubikey 4](https://github.com/drduh/YubiKey-Guide). # Creating a Key Pair # From c12e3b0e94432e3465900f6e031e065b0bedaba9 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Mon, 13 Aug 2018 16:17:54 -0700 Subject: [PATCH 6/6] remove .idea from .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 683d0c3..75894ea 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ boot/ .classpath .project .scala_dependencies -.idea/