Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable OpenPGP support via pinentry #142

Merged
merged 6 commits into from
Aug 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .java-version

This file was deleted.

1 change: 1 addition & 0 deletions pgp-plugin/src/main/scala/com/typesafe/sbt/SbtPgp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is ASetting vs BSetting?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rank is optional, and it affects the help display iirc. We should probably refactor it to sbt 0.13/1.x settingKey[Boolean] style.

val gpgAncient = SettingKey[Boolean]("gpg-ancient","Set this to true if you use a gpg version older than 2.1.")

// Checking PGP Signatures options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
)

Expand Down Expand Up @@ -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)
Expand All @@ -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
)

Expand Down
28 changes: 28 additions & 0 deletions pgp-plugin/src/main/scala/com/typesafe/sbt/pgp/PgpSigner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agent is pretty much required here

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}
Expand Down
9 changes: 9 additions & 0 deletions src/jekyll/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://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 -- 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 #

To create a key pair, enter the following:
Expand Down