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

Add signatory that supports GnuPG's gpg-agent #1703

Merged
merged 9 commits into from
Dec 27, 2017
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
42 changes: 42 additions & 0 deletions subprojects/docs/src/docs/userguide/signingPlugin.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,48 @@ OpenPGP supports subkeys, which are like the normal keys, except they're bound t

The signing plugin supports OpenPGP subkeys out of the box. Just specify a subkey ID as the value in the `signing.keyId` property.

[[sec:using_gpg_agent]]
=== Using gpg-agent

By default the signing plugin uses a Java-based implementation of PGP for signing. This implementation cannot use the gpg-agent program for managing private keys, though. If you want to use the gpg-agent, you can change the signatory implementation used by the signing plugin to api:org.gradle.plugins.signing.signatory.gnupg.GnupgSignatory[].

++++
<sample id="useGnupg" dir="signing/gnupg-signatory" title="Sign with GnuPG">
<sourcefile file="build.gradle" snippet="configure-signatory" />
</sample>
++++

This tells the signing plugin to use the `GnupgSignatory` instead of the default api:org.gradle.plugins.signing.signatory.pgp.PgpSignatory[]. The `GnupgSignatory` relies on the gpg2 progam to sign the artifacts. Of course, this requires that GnuPG is installed.

Without any further configuration the `gpg2` (on Windows: `gpg2.exe`) executable found on the `PATH` will be used. The password is supplied by the `gpg-agent` and the default key is used for signing.


[[sec:sec:gnupg_signatory_configuration]]
==== Gnupg signatory configuration

The `GnupgSignatory` supports a number of configuration options for controlling how gpg is invoked. These are typically set in gradle.properties:

++++
<sample id="configureGnupg" dir="signing/gnupg-signatory" title="Configure the GnupgSignatory">
<sourcefile file="gradle.properties" snippet="user-properties" />
</sample>
++++

`signing.gnupg.executable`::
The gpg executable that is invoked for signing. The default value of this property depends on `useLegacyGpg`. If that is `true` then the default value of executable is "gpg" otherwise it is "gpg2".
`signing.gnupg.useLegacyGpg`::
Must be `true` if GnuPG version 1 is used and `false` otherwise. The default value of the property is `false`.
`signing.gnupg.homeDir`::
Sets the home directory for GnuPG. If not given the default home directory of GnuPG is used.
`signing.gnupg.optionsFile`::
Sets a custom options file for GnuPG. If not given GnuPG's default configuration file is used.
`signing.gnupg.keyName`::
The id of the key that should be used for signing. If not given then the default key configured in GnuPG will be used.
`signing.gnupg.passphrase`::
The passphrase for unlocking the secret key. If not given then the gpg-agent program is used for getting the passphrase.

All configuration properties are optional.

[[sec:specifying_what_to_sign]]
=== Specifying what to sign

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apply plugin: 'java'
apply plugin: 'signing'

group = 'gradle'
version = '1.0'

// START SNIPPET configure-signatory
signing {
useGpgCmd()
sign configurations.archives
}
// END SNIPPET configure-signatory
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// START SNIPPET user-properties
signing.gnupg.executable=gpg
signing.gnupg.useLegacyGpg=true
signing.gnupg.homeDir=gnupg-home
signing.gnupg.optionsFile=gnupg-home/gpg.conf
signing.gnupg.keyName=24875D73
signing.gnupg.passphrase=gradle
// END SNIPPET user-properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class Sample {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some resource.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.gradle.plugins.signing

import org.gradle.test.fixtures.file.TestFile

import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

class GpgCmdFixture {
private static final Random RANDOM = new Random()
private static final int ALL_DIGITS_AND_LETTERS_RADIX = 36
private static final int MAX_RANDOM_PART_VALUE = Integer.valueOf("zzzzz", ALL_DIGITS_AND_LETTERS_RADIX)

static String createRandomPrefix() {
return Integer.toString(RANDOM.nextInt(MAX_RANDOM_PART_VALUE), ALL_DIGITS_AND_LETTERS_RADIX)
}

static String getAvailableGpg() {
if (tryRun('gpg2')) {
return 'gpg2'
} else if (tryRun('gpg')) {
return 'gpg'
} else {
return null
}
}

static boolean tryRun(String cmd) {
try {
"${cmd} --version".execute()
return true
}
catch (IOException e) {
return false
}
}

static setupGpgCmd(TestFile buildDir) {
def gpgHomeSymlink = prepareGnupgHomeSymlink(buildDir.file('gnupg-home'))
Properties properties = new Properties()
properties.load(buildDir.file('gradle.properties').newInputStream())
String client = GpgCmdFixture.getAvailableGpg()
assert client
properties.put('signing.gnupg.executable', client)
properties.put('signing.gnupg.useLegacyGpg', (client == 'gpg').toString())
properties.put('signing.gnupg.homeDir', gpgHomeSymlink.toAbsolutePath().toString())
properties.remove('signing.gnupg.optionsFile')
properties.store(buildDir.file('gradle.properties').newOutputStream(), '')

return gpgHomeSymlink
}

static cleanupGpgCmd(def gpgHomeSymlink) {
Files.deleteIfExists(gpgHomeSymlink)
}

static prepareGnupgHomeSymlink(File gpgHomeInTest) {
// We have to do this, otherwise gpg will complain: can't connect to the agent: File name too long
// it's limited to 108 chars due to http://man7.org/linux/man-pages/man7/unix.7.html
Path tmpLink = Paths.get(System.getProperty("java.io.tmpdir")).resolve(createRandomPrefix())
return Files.createSymbolicLink(tmpLink, gpgHomeInTest.toPath())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
package org.gradle.plugins.signing

class SigningConfigurationsIntegrationSpec extends SigningIntegrationSpec {

def "signing configurations"() {
given:
buildFile << """
Expand All @@ -25,22 +25,23 @@ class SigningConfigurationsIntegrationSpec extends SigningIntegrationSpec {
}

signing {
${signingConfiguration()}
sign configurations.archives, configurations.meta
}

${keyInfo.addAsPropertiesScript()}
${getJavadocAndSourceJarsScript("meta")}
"""

when:
run "buildSignatures"

then:
executedAndNotSkipped ":signArchives", ":signMeta"

and:
file("build", "libs", "sign-1.0.jar.asc").text
file("build", "libs", "sign-1.0-javadoc.jar.asc").text
file("build", "libs", "sign-1.0-sources.jar.asc").text
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.plugins.signing

import org.gradle.util.Requires

@Requires(adhoc = { GpgCmdFixture.getAvailableGpg() != null })
class SigningConfigurationsWithGpgCmdIntegrationSpec extends SigningConfigurationsIntegrationSpec {
SignMethod getSignMethod() {
return SignMethod.GPG_CMD
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,24 @@ package org.gradle.plugins.signing

import org.gradle.integtests.fixtures.AbstractIntegrationSpec
import org.gradle.integtests.fixtures.TestResources
import org.gradle.integtests.fixtures.executer.IntegrationTestBuildContext
import org.gradle.test.fixtures.file.TestFile
import org.junit.Rule

import java.nio.file.Path

import static org.gradle.util.TextUtil.escapeString

abstract class SigningIntegrationSpec extends AbstractIntegrationSpec {

enum SignMethod {
OPEN_GPG,
GPG_CMD
}

@Rule public final TestResources resources = new TestResources(temporaryFolder, "keys")

Path gpgHomeSymlink

String jarFileName = "sign-1.0.jar"

def setup() {
Expand All @@ -35,17 +45,42 @@ abstract class SigningIntegrationSpec extends AbstractIntegrationSpec {
group = 'sign'
version = '1.0'
"""

file("src", "main", "java", "Thing.java") << """
public class Thing {}
"""

if (getSignMethod() == SignMethod.GPG_CMD) {
setupGpgCmd()
}
}

def setupGpgCmd() {
TestFile sampleDir = new IntegrationTestBuildContext().getSamplesDir()
sampleDir.file('signing/gnupg-signatory/gnupg-home').copyTo(file('gnupg-home'))
sampleDir.file('signing/gnupg-signatory/gradle.properties').copyTo(file('gradle.properties'))
GpgCmdFixture.setupGpgCmd(temporaryFolder.testDirectory)
}

def cleanup() {
if (gpgHomeSymlink != null) {
GpgCmdFixture.cleanupGpgCmd(gpgHomeSymlink)
}
}

def signingConfiguration() {
if (getSignMethod() == SignMethod.OPEN_GPG) {
return ''
} else {
return 'useGpgCmd()'
}
}

static class KeyInfo {
String keyId
String password
String keyRingFilePath

Map<String, String> asProperties(String name = null) {
def prefix = name ? "signing.${name}." : "signing."
def properties = [:]
Expand All @@ -54,22 +89,22 @@ abstract class SigningIntegrationSpec extends AbstractIntegrationSpec {
properties[prefix + "secretKeyRingFile"] = keyRingFilePath
properties
}

String addAsPropertiesScript(addTo = "project.ext", name = null) {
asProperties(name).collect { k, v ->
"${addTo}.setProperty('${escapeString(k)}', '${escapeString(v)}')"
}.join(";")
}
}

KeyInfo getKeyInfo(set = "default") {
new KeyInfo(
keyId: file(set, "keyId.txt").text.trim(),
password: file(set, "password.txt").text.trim(),
keyRingFilePath: file(set, "secring.gpg")
)
}

String getJavadocAndSourceJarsScript(String configurationName = null) {
def tasks = """
task("sourcesJar", type: Jar, dependsOn: classes) {
Expand All @@ -82,7 +117,7 @@ abstract class SigningIntegrationSpec extends AbstractIntegrationSpec {
from javadoc.destinationDir
}
"""

if (configurationName == null) {
tasks
} else {
Expand All @@ -97,7 +132,7 @@ abstract class SigningIntegrationSpec extends AbstractIntegrationSpec {
"""
}
}

String uploadArchives() {
return """
apply plugin: "maven"
Expand Down Expand Up @@ -168,12 +203,16 @@ abstract class SigningIntegrationSpec extends AbstractIntegrationSpec {
}
"""
}

File pom(String name = "sign-1.0") {
m2RepoFile("${name}.pom")
}

File pomSignature(String name = "sign-1.0") {
m2RepoFile("${name}.pom.asc")
}
}

SignMethod getSignMethod() {
return SignMethod.OPEN_GPG
}
}
Loading