diff --git a/pom.xml b/pom.xml index abcc0fd..8869f6a 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,7 @@ under the License. 3.0 7 2020-04-12T12:45:04Z + @ @@ -145,9 +146,33 @@ under the License. 2.2 test + + org.apache.maven.shared + maven-invoker + 3.1.0 + test + + + + ${basedir}/src/test/resources + true + + **/pom.xml + **/settings.xml + + + + ${basedir}/src/test/resources + + **/pom.xml + **/settings.xml + + + + @@ -205,6 +230,30 @@ under the License. run-its + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + true + + + ${maven.home} + ${https.protocols} + ${project.build.testOutputDirectory}/gnupg + ${project.build.directory}/local-repo + /it/settings.xml + + + org.apache.maven.plugins maven-invoker-plugin diff --git a/src/it/sign-release-without-passphrase/invoker.properties b/src/it/sign-release-without-passphrase/invoker.properties new file mode 100644 index 0000000..f2a7dfb --- /dev/null +++ b/src/it/sign-release-without-passphrase/invoker.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +invoker.buildResult = failure diff --git a/src/it/sign-release-without-passphrase/pom.xml b/src/it/sign-release-without-passphrase/pom.xml new file mode 100644 index 0000000..8e279f5 --- /dev/null +++ b/src/it/sign-release-without-passphrase/pom.xml @@ -0,0 +1,100 @@ + + + + + + 4.0.0 + + org.apache.maven.its.gpg.srwop + test + 1.0 + jar + + + Tests that signing with a missing passphrase in Maven batch mode (non-interactive mode) does not hang. + + + + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.0.2 + + + org.apache.maven.plugins + maven-gpg-plugin + @project.version@ + + non-existent + + + + sign-artifacts + + sign + + + + + + org.apache.maven.plugins + maven-install-plugin + 2.2 + + true + + + + org.apache.maven.plugins + maven-jar-plugin + 2.1 + + + org.apache.maven.plugins + maven-resources-plugin + 2.2 + + + org.apache.maven.plugins + maven-source-plugin + 2.0.4 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.3.1 + + + + + diff --git a/src/it/sign-release-without-passphrase/verify.bsh b/src/it/sign-release-without-passphrase/verify.bsh new file mode 100644 index 0000000..8ff268c --- /dev/null +++ b/src/it/sign-release-without-passphrase/verify.bsh @@ -0,0 +1,38 @@ + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import java.io.*; +import org.codehaus.plexus.util.FileUtils; + +File buildLog = new File( basedir, "build.log" ); +String logContent = FileUtils.fileRead(buildLog); + +// assert that the Maven build properly failed and did not time out +if ( !logContent.contains( "Total time: " ) || !logContent.contains( "Finished at: " ) ) +{ + throw new Exception( "Maven build did not fail, but timed out" ); +} + +// assert that the Maven build failed, because pinentry is not allowed in non-interactive mode +if ( !logContent.contains( "[GNUPG:] FAILURE sign 67108949" ) ) +{ + throw new Exception( "Maven build did not fail in consequence of pinentry not being available to GPG" ); +} + diff --git a/src/main/java/org/apache/maven/plugins/gpg/GpgSigner.java b/src/main/java/org/apache/maven/plugins/gpg/GpgSigner.java index 65dd396..3fb0f3e 100644 --- a/src/main/java/org/apache/maven/plugins/gpg/GpgSigner.java +++ b/src/main/java/org/apache/maven/plugins/gpg/GpgSigner.java @@ -97,20 +97,25 @@ protected void generateSignatureForFile( File file, File signature ) cmd.createArg().setValue( "--no-use-agent" ); } } - else - { - cmd.createArg().setValue( "--pinentry-mode" ); - cmd.createArg().setValue( "loopback" ); - } InputStream in = null; if ( null != passphrase ) { - // make --passphrase-fd effective in gpg2 - cmd.createArg().setValue( "--batch" ); + if ( gpgVersion.isAtLeast( GpgVersion.parse( "2.0" ) ) ) + { + // required for option --passphrase-fd since GPG 2.0 + cmd.createArg().setValue( "--batch" ); + } - cmd.createArg().setValue( "--passphrase-fd" ); + if ( gpgVersion.isAtLeast( GpgVersion.parse( "2.1" ) ) ) + { + // required for option --passphrase-fd since GPG 2.1 + cmd.createArg().setValue( "--pinentry-mode" ); + cmd.createArg().setValue( "loopback" ); + } + // make --passphrase-fd effective in gpg2 + cmd.createArg().setValue( "--passphrase-fd" ); cmd.createArg().setValue( "0" ); // Prepare the input stream which will be used to pass the passphrase to the executable @@ -128,9 +133,24 @@ protected void generateSignatureForFile( File file, File signature ) cmd.createArg().setValue( "--detach-sign" ); + if ( getLog().isDebugEnabled() ) + { + // instruct GPG to write status information to stdout + cmd.createArg().setValue( "--status-fd" ); + cmd.createArg().setValue( "1" ); + } + if ( !isInteractive ) { + cmd.createArg().setValue( "--batch" ); cmd.createArg().setValue( "--no-tty" ); + + if ( null == passphrase && gpgVersion.isAtLeast( GpgVersion.parse( "2.1" ) ) ) + { + // prevent GPG from spawning input prompts in Maven non-interactive mode + cmd.createArg().setValue( "--pinentry-mode" ); + cmd.createArg().setValue( "error" ); + } } if ( !defaultKeyring ) diff --git a/src/test/java/org/apache/maven/plugins/gpg/it/BuildResult.java b/src/test/java/org/apache/maven/plugins/gpg/it/BuildResult.java new file mode 100644 index 0000000..3e40566 --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/gpg/it/BuildResult.java @@ -0,0 +1,48 @@ +package org.apache.maven.plugins.gpg.it; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import java.io.File; + +import org.apache.maven.shared.invoker.InvocationResult; + +public class BuildResult +{ + + private final File buildLog; + private final InvocationResult invocationResult; + + public BuildResult( final File buildLog, final InvocationResult invocationResult ) + { + this.buildLog = buildLog; + this.invocationResult = invocationResult; + } + + public File getBuildLog() + { + return buildLog; + } + + public InvocationResult getInvocationResult() + { + return invocationResult; + } + +} diff --git a/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignAttachedMojoIT.java b/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignAttachedMojoIT.java new file mode 100644 index 0000000..ea36ac1 --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignAttachedMojoIT.java @@ -0,0 +1,74 @@ +package org.apache.maven.plugins.gpg.it; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +import java.io.File; + +import org.apache.maven.shared.invoker.InvocationRequest; +import org.apache.maven.shared.invoker.InvocationResult; +import org.codehaus.plexus.util.FileUtils; +import org.junit.Test; + +public class GpgSignAttachedMojoIT +{ + + private final File mavenHome; + private final File localRepository; + private final File mavenUserSettings; + private final File gpgHome; + + public GpgSignAttachedMojoIT() throws Exception + { + this.mavenHome = new File( System.getProperty( "maven.home" ) ); + this.localRepository = new File( System.getProperty( "localRepositoryPath" ) ); + this.mavenUserSettings = InvokerTestUtils.getTestResource( System.getProperty( "settingsFile" ) ); + this.gpgHome = new File( System.getProperty( "gpg.homedir" ) ); + } + + @Test + public void testInteractiveWithoutPassphrase() throws Exception + { + // given + final File pomFile = InvokerTestUtils.getTestResource( "/it/sign-release-without-passphrase-interactive/pom.xml" ); + final InvocationRequest request = InvokerTestUtils.createRequest( pomFile, mavenUserSettings, gpgHome ); + + // require Maven interactive mode + request.setBatchMode( false ); + + // when + final BuildResult result = InvokerTestUtils.executeRequest( request, mavenHome, localRepository ); + + final InvocationResult invocationResult = result.getInvocationResult(); + final String buildLogContent = FileUtils.fileRead( result.getBuildLog() ); + + // then + assertThat( "Maven execution must fail", invocationResult.getExitCode(), not( 0 ) ); + assertThat( + "Maven execution failed because no pinentry program is available", + buildLogContent, + containsString( "[GNUPG:] FAILURE sign 67108949" ) + ); + } + +} diff --git a/src/test/java/org/apache/maven/plugins/gpg/it/InvokerTestUtils.java b/src/test/java/org/apache/maven/plugins/gpg/it/InvokerTestUtils.java new file mode 100644 index 0000000..3dd041d --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/gpg/it/InvokerTestUtils.java @@ -0,0 +1,106 @@ +package org.apache.maven.plugins.gpg.it; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Arrays; +import java.util.Properties; + +import org.apache.commons.io.input.NullInputStream; +import org.apache.maven.shared.invoker.DefaultInvocationRequest; +import org.apache.maven.shared.invoker.DefaultInvoker; +import org.apache.maven.shared.invoker.InvocationOutputHandler; +import org.apache.maven.shared.invoker.InvocationRequest; +import org.apache.maven.shared.invoker.InvocationResult; +import org.apache.maven.shared.invoker.Invoker; +import org.apache.maven.shared.invoker.InvokerLogger; +import org.apache.maven.shared.invoker.MavenInvocationException; +import org.apache.maven.shared.invoker.PrintStreamHandler; +import org.apache.maven.shared.invoker.PrintStreamLogger; + +public class InvokerTestUtils +{ + + public static InvocationRequest createRequest( final File pomFile, final File mavenUserSettings, final File gpgHome ) + { + final InvocationRequest request = new DefaultInvocationRequest(); + request.setUserSettingsFile( mavenUserSettings ); + request.setShowVersion( true ); + request.setDebug( true ); + request.setShowErrors( true ); + request.setTimeoutInSeconds( 60 ); // safeguard against GPG freezes + request.setGoals( Arrays.asList( "clean", "install" ) ); + request.setPomFile( pomFile ); + + final Properties properties = new Properties(); + request.setProperties( properties ); + + // Required for JRE 7 to connect to Maven Central with TLSv1.2 + final String httpsProtocols = System.getProperty( "https.protocols" ); + if ( httpsProtocols != null && !httpsProtocols.isEmpty() ) { + properties.setProperty( "https.protocols", httpsProtocols ); + } + + properties.setProperty( "gpg.homedir", gpgHome.getAbsolutePath() ); + + return request; + } + + public static BuildResult executeRequest( final InvocationRequest request, final File mavenHome, final File localRepository ) + throws FileNotFoundException, MavenInvocationException + { + final InvocationResult result; + + final File buildLog = new File( request.getBaseDirectory( request.getPomFile().getParentFile() ), "build.log" ); + try ( final PrintStream buildLogStream = new PrintStream( buildLog ) ) + { + final InvocationOutputHandler buildLogOutputHandler = new PrintStreamHandler( buildLogStream, false ); + final InvokerLogger logger = new PrintStreamLogger( buildLogStream, InvokerLogger.DEBUG ); + + final Invoker invoker = new DefaultInvoker(); + invoker.setMavenHome( mavenHome ); + invoker.setLocalRepositoryDirectory( localRepository ); + invoker.setInputStream( new NullInputStream( 0 ) ); + invoker.setOutputHandler( buildLogOutputHandler ); + invoker.setErrorHandler( buildLogOutputHandler ); + invoker.setLogger( logger ); + + result = invoker.execute( request ); + } + + return new BuildResult( buildLog, result ); + } + + public static File getTestResource( final String path ) throws URISyntaxException, FileNotFoundException + { + final URL resourceUrl = InvokerTestUtils.class.getResource( path ); + if ( resourceUrl == null ) + { + throw new FileNotFoundException( "Cannot find file " + path ); + } + + return new File( resourceUrl.toURI() ); + } + +} diff --git a/src/test/resources/gnupg/gpg-agent.conf b/src/test/resources/gnupg/gpg-agent.conf new file mode 100644 index 0000000..00efc6f --- /dev/null +++ b/src/test/resources/gnupg/gpg-agent.conf @@ -0,0 +1,5 @@ +# Prevent gpg-agent from caching the passphrase / unlocked key between integration tests +ignore-cache-for-signing + +# Prevent pinentry input prompts from blocking integration tests +pinentry-program pinentry-non-existent diff --git a/src/test/resources/it/settings.xml b/src/test/resources/it/settings.xml new file mode 100644 index 0000000..bc80ff2 --- /dev/null +++ b/src/test/resources/it/settings.xml @@ -0,0 +1,59 @@ + + + + + + + + + it-repo + + true + + + + local.central + file://@settings.localRepository@ + + true + + + true + + + + + + local.central + file://@settings.localRepository@ + + true + + + true + + + + + + + diff --git a/src/test/resources/it/sign-release-without-passphrase-interactive/pom.xml b/src/test/resources/it/sign-release-without-passphrase-interactive/pom.xml new file mode 100644 index 0000000..64de500 --- /dev/null +++ b/src/test/resources/it/sign-release-without-passphrase-interactive/pom.xml @@ -0,0 +1,97 @@ + + + + + + 4.0.0 + + org.apache.maven.its.gpg.srwopi + test + 1.0 + jar + + + Tests that signing with a missing passphrase in Maven interactive mode does not hang. + + + + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.0.2 + + + org.apache.maven.plugins + maven-gpg-plugin + @project.version@ + + + sign-artifacts + + sign + + + + + + org.apache.maven.plugins + maven-install-plugin + 2.2 + + true + + + + org.apache.maven.plugins + maven-jar-plugin + 2.1 + + + org.apache.maven.plugins + maven-resources-plugin + 2.2 + + + org.apache.maven.plugins + maven-source-plugin + 2.0.4 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.3.1 + + + + +