Skip to content

Commit

Permalink
Enhance the PGP signing support.
Browse files Browse the repository at this point in the history
Improve support for how already-PGP-signed artifacts are handled to
support skip, replace, or merge.

Use SignedContentFactory for checking jar signatures which allows for
skipping jars that are only signed by certificates anchored in Java's
cacerts.

Support PGP signing features, and optionally also binary artifacts.

Support using Bouncy Castle for signing to improve performance and to
allow signing to be done in parallel.  This also better support
providing integration tests for the various sign-p2-artifacts mojo's
options.

Ensure that only keys actually used by signatures are added to the
repository and/or artifact properties.  Determining the default key is
needed only when signing with Bouncy Castle, in which case it's
determined by signing a document and checking which key is used; that's
because the default key can be specified in the gpg.conf so just
listing the secret key fingerprints will not always correctly identify
the correct default.

For testing purposes, support generating key information and loading it.
Use that to provide integration tests for the new options as well as for
existing options.

eclipse-tycho#1466
  • Loading branch information
merks committed Nov 24, 2022
1 parent ff207cc commit 1b2b406
Show file tree
Hide file tree
Showing 18 changed files with 1,589 additions and 252 deletions.
11 changes: 11 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ This page describes the noteworthy improvements provided by each release of Ecli

### Migration guide 3.x > 4.x

### PGP Signing Enhancements

The [tycho-gpg::3.0.0:sign-p2-artifacts](https://tycho.eclipseprojects.io/doc/3.0.0/tycho-gpg-plugin/sign-p2-artifacts-mojo.html) mojo has been significantly enhanced.

The following properties have been added:

- `skipIfJarsignedAndAnchored` - This is similar to `skipIfJarsigned` but is weaker in the sense that the signatures are checked in detail such that the PGP signing is skipped if and only if one of the signatures is anchored in Java cacerts. The default is `false`. Set `skipIfJarsignedAndAnchored` to `true` and `skipIfJarsigned` to `false` to enable this feature.
- `skipBinaries` - Setting this to `false` will enable the signing of binary artifacts, which are of course not jar-signed.
- `pgpKeyBehavor` - Specify `skip`, `replace`, or `merge` for how to handle the signing of artifacts that are already PGP signed.
- `signer` - Currently supported are `bc` and `gpg` where the former is a new implementation that uses Bouncy Castle for signing, which is significantly faster and allows signing to proceed in parallel. This can also be configured by the system property `tycho.pgp.signer`.

#### mixed reactor setups require the new resolver now

If you want to use so called mixed-reactor setups, that is you have bundles build by other techniques than Tycho (e.g. bnd/felix-maven-plugin) mixed with ones build by Tycho,
Expand Down
2 changes: 1 addition & 1 deletion src/site/site.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<item name="p2 Repository Plugin" href="tycho-p2/tycho-p2-repository-plugin/plugin-info.html" />
<item name="p2 Director Plugin" href="tycho-p2/tycho-p2-director-plugin/plugin-info.html" />
<item name="Source Plugin" href="tycho-source-plugin/plugin-info.html" />
<item name="GPG Signature plkugin" href="tycho-gpg-plugin/plugin-info.html"/>
<item name="GPG Signature Plugin" href="tycho-gpg-plugin/plugin-info.html"/>
<item name="Versions Plugin" href="tycho-release/tycho-versions-plugin/plugin-info.html" />
<item name="Properties">
<item name="Build Properties" href="BuildProperties.html" />
Expand Down
16 changes: 12 additions & 4 deletions tycho-gpg-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,19 @@
<version>2.6.200</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.1</version>
<scope>test</scope>
<groupId>org.eclipse.tycho</groupId>
<artifactId>tycho-core</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-component-metadata</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
*******************************************************************************/
package org.apache.maven.plugins.gpg;

import java.io.File;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
Expand All @@ -18,7 +20,14 @@ public abstract class AbstractGpgMojoExtension extends AbstractGpgMojo {
@Override
protected ProxySignerWithPublicKeyAccess newSigner(MavenProject project)
throws MojoExecutionException, MojoFailureException {
return new ProxySignerWithPublicKeyAccess(super.newSigner(project));
return new ProxySignerWithPublicKeyAccess(super.newSigner(project), getSigner(), getPGPInfo());
}

protected String getSigner() {
return "gpg";
}

protected File getPGPInfo() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,131 @@
*******************************************************************************/
package org.apache.maven.plugins.gpg;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.bouncycastle.openpgp.PGPException;
import org.codehaus.plexus.util.Os;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.CommandLineUtils.StringStreamConsumer;
import org.codehaus.plexus.util.cli.Commandline;
import org.eclipse.equinox.p2.repository.spi.PGPPublicKeyService;
import org.eclipse.tycho.gpg.BouncyCastleSigner;
import org.eclipse.tycho.gpg.KeyStore;
import org.eclipse.tycho.gpg.SignatureStore;

public class ProxySignerWithPublicKeyAccess extends AbstractGpgSigner {

private AbstractGpgSigner delegate;
private final AbstractGpgSigner delegate;

public ProxySignerWithPublicKeyAccess(AbstractGpgSigner newSigner) {
this.delegate = newSigner;
private final BouncyCastleSigner signer;

private KeyStore publicKeys;

public ProxySignerWithPublicKeyAccess(AbstractGpgSigner delegate, String signer, File pgpInfo) {
this.delegate = delegate;
this.setLog(delegate.getLog());
// The pgpInfo is used only for testing purposes.
if ("bc".equals(signer) || pgpInfo != null) {
try {
this.signer = getSigner(pgpInfo);
this.signer.setLog(getLog());
} catch (MojoExecutionException | MojoFailureException | IOException | PGPException e) {
throw new RuntimeException(e);
}
} else {
this.signer = null;
}
}

public KeyStore getPublicKeys() {
if (publicKeys == null) {
try {
publicKeys = KeyStore.create(getKeys(true));
} catch (MojoExecutionException e) {
new RuntimeException(e.getMessage(), e);
}
}
return publicKeys;
}

protected BouncyCastleSigner getSigner(File pgpInfo)
throws MojoExecutionException, IOException, MojoFailureException, PGPException {
keyname = delegate.keyname;
if (pgpInfo != null) {
var signer = new BouncyCastleSigner(keyname, pgpInfo);
publicKeys = KeyStore.create(signer.getPublicKeys());
return signer;
} else {
var publicKeys = getPublicKeys().toArmoredString();
var secretKeys = getKeys(false);
if (keyname == null) {
// Determine which key is used for signing by signing a file.
var dummy = Files.createTempFile("dummy", ".txt");
var signature = Files.createTempFile("dummy", ".asc");
Files.delete(signature);
delegate.generateSignatureForFile(dummy.toFile(), signature.toFile());
var signatures = SignatureStore.create(Files.readString(signature, StandardCharsets.US_ASCII));
keyname = PGPPublicKeyService.toHex(signatures.all().iterator().next().getKeyID());
Files.delete(dummy);
Files.delete(signature);
}
return new BouncyCastleSigner(keyname, delegate.passphrase, publicKeys, secretKeys);
}
}

public SignatureStore generateSignature(File file) throws MojoExecutionException {
try {
if (signer != null) {
return signer.generateSignature(file);
} else {
File signatureFile;
synchronized (delegate) {
// gpg generally doesn't like to sign in parallel.
signatureFile = delegate.generateSignatureForArtifact(file);
}
var signatureStore = SignatureStore
.create(Files.readString(signatureFile.toPath(), StandardCharsets.US_ASCII));
signatureFile.delete();
return signatureStore;
}
} catch (Exception e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}

@Override
protected void generateSignatureForFile(File file, File signature) throws MojoExecutionException {
delegate.generateSignatureForFile(file, signature);
if (signer != null) {
try {
Files.writeString(signature.toPath(), signer.generateSignature(file).toArmoredString());
} catch (IOException | PGPException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
} else {
delegate.generateSignatureForFile(file, signature);
}
}

protected Commandline getDefaultGpgCommandLine() {
Commandline cmd = new Commandline();
/**
* Fetches the public or secrete keys using gpg.
*/
private String getKeys(boolean isPublic) throws MojoExecutionException {
var cmd = new Commandline();

// if ( StringUtils.isNotEmpty( executable ) ) {
// cmd.setExecutable( executable );
// } else {
cmd.setExecutable("gpg" + (Os.isFamily(Os.FAMILY_WINDOWS) ? ".exe" : ""));
// }
var executable = "gpg" + (Os.isFamily(Os.FAMILY_WINDOWS) ? ".exe" : "");
cmd.setExecutable(executable);

if (delegate.args != null) {
for (String arg : delegate.args) {
for (var arg : delegate.args) {
cmd.createArg().setValue(arg);
}
}
Expand All @@ -55,6 +143,46 @@ protected Commandline getDefaultGpgCommandLine() {
cmd.createArg().setFile(delegate.homeDir);
}

InputStream in = null;
if (isPublic) {
cmd.createArg().setValue("--export");
} else {
cmd.createArg().setValue("--export-secret-keys");
if (delegate.passphrase != null) {
var versionParser = GpgVersionParser.parse(executable);
var gpgVersion = versionParser.getGpgVersion();
if (gpgVersion.isAtLeast(GpgVersion.parse("2.0"))) {
// required for option --passphrase-fd since GPG 2.0
cmd.createArg().setValue("--batch");
}

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
in = new ByteArrayInputStream(delegate.passphrase.getBytes());

if (StringUtils.isNotEmpty(delegate.secretKeyring)) {
if (gpgVersion.isBefore(GpgVersion.parse("2.1"))) {
cmd.createArg().setValue("--secret-keyring");
cmd.createArg().setValue(delegate.secretKeyring);
} else {
getLog().warn("'secretKeyring' is an obsolete option and ignored. All secret keys "
+ "are stored in the ‘private-keys-v1.d’ directory below the GnuPG home directory.");
}
}
}
}

cmd.createArg().setValue("--armor");

if (!delegate.defaultKeyring) {
cmd.createArg().setValue("--no-default-keyring");
}
Expand All @@ -63,13 +191,18 @@ protected Commandline getDefaultGpgCommandLine() {
cmd.createArg().setValue("--keyring");
cmd.createArg().setValue(delegate.publicKeyring);
}
return cmd;
}

static String executeAndGetOutput(Commandline cmd) throws MojoExecutionException {
if (delegate.keyname != null) {
cmd.createArg().setValue(delegate.keyname);
}

// ----------------------------------------------------------------------------
// Execute the command line
// ----------------------------------------------------------------------------

try {
StringStreamConsumer systemOut = new StringStreamConsumer();
int exitCode = CommandLineUtils.executeCommandLine(cmd, null, systemOut, systemOut);
var systemOut = new StringStreamConsumer();
var exitCode = CommandLineUtils.executeCommandLine(cmd, in, systemOut, systemOut);
if (exitCode != 0) {
throw new MojoExecutionException("Exit code: " + exitCode);
}
Expand All @@ -78,45 +211,4 @@ static String executeAndGetOutput(Commandline cmd) throws MojoExecutionException
throw new MojoExecutionException("Unable to execute gpg command", e);
}
}

String getDefaultKeyFingerprint() throws MojoExecutionException, IOException {
Commandline cmd = getDefaultGpgCommandLine();
cmd.createArg().setValue("--list-secret-keys");
cmd.createArg().setValue("--with-colons");
return extractFingerprint(executeAndGetOutput(cmd));
}

static String extractFingerprint(String output) throws IOException {
try (BufferedReader reader = new BufferedReader(new StringReader(output))) {
String fprLine = reader.lines().filter(l -> l.startsWith("fpr")).findFirst().orElse("");
String[] parts = fprLine.split(":");
if (parts.length < 10) {
throw new IllegalArgumentException(
"Could not extract first fingerprint from output: " + System.lineSeparator() + output);
}
return parts[9];
}
}

public String getPublicKeys() throws MojoExecutionException {
Commandline cmd = getDefaultGpgCommandLine();

cmd.createArg().setValue("--export");
cmd.createArg().setValue("--armor");

if (delegate.keyname != null) {
cmd.createArg().setValue(delegate.keyname);
} else {
try {
String defaultKeyFingerprint = getDefaultKeyFingerprint();
getLog().info("Using public key of first secret keypair \"" + defaultKeyFingerprint + "\"");
cmd.createArg().setValue(defaultKeyFingerprint);
} catch (IOException | IllegalArgumentException e) {
throw new MojoExecutionException("Could not determine default fingerprint", e);
}
}

return executeAndGetOutput(cmd);
}

}
Loading

0 comments on commit 1b2b406

Please sign in to comment.