Bug 572816 - p2 strategy to trust PGP signatures

This makes users declare whether PGP keys are trusted or not at
installation, and to skip installation if one artifact has no
signature/signer being trusted.

* Propagate the pgp.signatures on local artifact description, so it's
usable for CheckTrust
* Add support in the Trust model for PGP keys
* Add (limited) support for PGP approval in TrustCertificationDialog
* Skip installation is PGP Keys are not trusted (similarly to
certificates).

Current limitations:
* Dialog doesn't show whether a subset of PGP Keys is sufficient to
complete installation (eg 1 artifact may have mulitple signature, only 1
is necessary to be approved for installation to complete, dialog doesn't
show that and gives impression all keys need to be approved)
* The dialog doesn't give any form of hint about how to decide whether
to trust a key or net (eg check PGP key registries and so on); but it's
also the case for certificates apparently...

Change-Id: I65f698c7412027fedefc28ddfaa344caa6bfecdc
# Conflicts:
#	bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/dialogs/TrustCertificateDialog.java
Reviewed-on: https://git.eclipse.org/r/c/equinox/rt.equinox.p2/+/179275
Tested-by: Equinox Bot <equinox-bot@eclipse.org>
Reviewed-by: Mickael Istria <mistria@redhat.com>
diff --git a/bundles/org.eclipse.equinox.p2.artifact.repository/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.p2.artifact.repository/META-INF/MANIFEST.MF
index 6c49a76..6023a8c 100644
--- a/bundles/org.eclipse.equinox.p2.artifact.repository/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.p2.artifact.repository/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.equinox.p2.artifact.repository;singleton:=true
-Bundle-Version: 1.4.100.qualifier
+Bundle-Version: 1.4.200.qualifier
 Bundle-Activator: org.eclipse.equinox.internal.p2.artifact.repository.Activator
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
@@ -10,7 +10,7 @@
  org.eclipse.equinox.internal.p2.artifact.processors.checksum;x-friends:="org.eclipse.equinox.p2.publisher",
  org.eclipse.equinox.internal.p2.artifact.processors.md5;x-internal:=true,
  org.eclipse.equinox.internal.p2.artifact.processors.pack200;x-friends:="org.eclipse.equinox.p2.artifact.processors,org.eclipse.equinox.p2.artifact.optimizers",
- org.eclipse.equinox.internal.p2.artifact.processors.pgp;x-internal:=true,
+ org.eclipse.equinox.internal.p2.artifact.processors.pgp;x-friends:="org.eclipse.equinox.p2.engine",
  org.eclipse.equinox.internal.p2.artifact.repository;
   x-friends:="org.eclipse.equinox.p2.publisher,
    org.eclipse.equinox.p2.reconciler.dropins,
diff --git a/bundles/org.eclipse.equinox.p2.artifact.repository/pom.xml b/bundles/org.eclipse.equinox.p2.artifact.repository/pom.xml
index 4826e17..3e4f3e6 100644
--- a/bundles/org.eclipse.equinox.p2.artifact.repository/pom.xml
+++ b/bundles/org.eclipse.equinox.p2.artifact.repository/pom.xml
@@ -9,6 +9,6 @@
   </parent>
   <groupId>org.eclipse.equinox</groupId>
   <artifactId>org.eclipse.equinox.p2.artifact.repository</artifactId>
-  <version>1.4.100-SNAPSHOT</version>
+  <version>1.4.200-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 </project>
diff --git a/bundles/org.eclipse.equinox.p2.artifact.repository/src/org/eclipse/equinox/internal/p2/artifact/processors/pgp/PGPSignatureVerifier.java b/bundles/org.eclipse.equinox.p2.artifact.repository/src/org/eclipse/equinox/internal/p2/artifact/processors/pgp/PGPSignatureVerifier.java
index e81fc0e..38439d6 100644
--- a/bundles/org.eclipse.equinox.p2.artifact.repository/src/org/eclipse/equinox/internal/p2/artifact/processors/pgp/PGPSignatureVerifier.java
+++ b/bundles/org.eclipse.equinox.p2.artifact.repository/src/org/eclipse/equinox/internal/p2/artifact/processors/pgp/PGPSignatureVerifier.java
@@ -12,6 +12,7 @@
 
 import java.io.*;
 import java.util.*;
+import java.util.stream.Collectors;
 import org.bouncycastle.bcpg.ArmoredInputStream;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.bouncycastle.openpgp.*;
@@ -40,15 +41,40 @@
 	 */
 	public static final String ID = "org.eclipse.equinox.p2.processing.PGPSignatureCheck"; //$NON-NLS-1$
 
+	private static Map<Long, PGPPublicKey> knownKeys = new HashMap<>();
+
 	public static final String PGP_SIGNER_KEYS_PROPERTY_NAME = "pgp.publicKeys"; //$NON-NLS-1$
 	public static final String PGP_SIGNATURES_PROPERTY_NAME = "pgp.signatures"; //$NON-NLS-1$
-	private List<PGPSignature> signaturesToVerify;
+	private Collection<PGPSignature> signaturesToVerify;
 
 	public PGPSignatureVerifier() {
 		super();
 		link(nullOutputStream(), new NullProgressMonitor()); // this is convenience for tests
 	}
 
+	private static Collection<PGPSignature> getSignatures(IArtifactDescriptor artifact)
+			throws IOException, PGPException {
+		String signatureText = unnormalizedPGPProperty(artifact.getProperty(PGP_SIGNATURES_PROPERTY_NAME));
+		if (signatureText == null) {
+			return Collections.emptyList();
+		}
+		List<PGPSignature> res = new ArrayList<>();
+		try (InputStream in = new ArmoredInputStream(new ByteArrayInputStream(signatureText.getBytes()))) {
+			JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(in);
+			Object o = pgpFactory.nextObject();
+			PGPSignatureList signatureList = new PGPSignatureList(new PGPSignature[0]);
+			if (o instanceof PGPCompressedData) {
+				PGPCompressedData pgpCompressData = (PGPCompressedData) o;
+				pgpFactory = new JcaPGPObjectFactory(pgpCompressData.getDataStream());
+				signatureList = (PGPSignatureList) pgpFactory.nextObject();
+			} else if (o instanceof PGPSignatureList) {
+				signatureList = (PGPSignatureList) o;
+			}
+			signatureList.iterator().forEachRemaining(res::add);
+		}
+		return res;
+	}
+
 	@Override
 	public void initialize(IProvisioningAgent agent, IProcessingStepDescriptor descriptor,
 			IArtifactDescriptor context) {
@@ -60,23 +86,8 @@
 			setStatus(Status.OK_STATUS);
 			return;
 		}
-		signaturesToVerify = new ArrayList<>();
-		try (InputStream in = new ArmoredInputStream(new ByteArrayInputStream(signatureText.getBytes()))) {
-			JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(in);
-			Object o = pgpFactory.nextObject();
-			PGPSignatureList signatureList;
-			if (o instanceof PGPCompressedData) {
-				PGPCompressedData pgpCompressData = (PGPCompressedData) o;
-				pgpFactory = new JcaPGPObjectFactory(pgpCompressData.getDataStream());
-				signatureList = (PGPSignatureList) pgpFactory.nextObject();
-			} else if (o instanceof PGPSignatureList) {
-				signatureList = (PGPSignatureList) o;
-			} else {
-				setStatus(new Status(IStatus.ERROR, Activator.ID,
-						Messages.Error_CouldNotLoadSignature));
-				return;
-			}
-			signatureList.iterator().forEachRemaining(signaturesToVerify::add);
+		try {
+			signaturesToVerify = getSignatures(context);
 		} catch (Exception ex) {
 			setStatus(new Status(IStatus.ERROR, Activator.ID, Messages.Error_CouldNotLoadSignature, ex));
 			return;
@@ -87,10 +98,10 @@
 		}
 
 		IArtifactRepository repository = context.getRepository();
-		Map<Long, PGPPublicKey> signerKeys = readPublicKeys(context.getProperty(PGP_SIGNER_KEYS_PROPERTY_NAME),
-				repository != null ? repository.getProperty(PGP_SIGNER_KEYS_PROPERTY_NAME) : null);
+		knownKeys.putAll(readPublicKeys(context.getProperty(PGP_SIGNER_KEYS_PROPERTY_NAME),
+				repository != null ? repository.getProperty(PGP_SIGNER_KEYS_PROPERTY_NAME) : null));
 		for (PGPSignature signature : signaturesToVerify) {
-			PGPPublicKey publicKey = signerKeys.get(signature.getKeyID());
+			PGPPublicKey publicKey = knownKeys.get(signature.getKeyID());
 			if (publicKey == null) {
 				setStatus(new Status(IStatus.ERROR, Activator.ID,
 						NLS.bind(Messages.Error_publicKeyNotFound, signature.getKeyID())));
@@ -114,7 +125,7 @@
 	 * @param pgpSignaturesPropertyName
 	 * @return fixed PGP armored blocks
 	 */
-	private String unnormalizedPGPProperty(String value) {
+	private static String unnormalizedPGPProperty(String value) {
 		if (value == null) {
 			return null;
 		}
@@ -124,7 +135,7 @@
 				.replace("-----END\nPGP\nPUBLIC\nKEY\nBLOCK-----", "-----END PGP PUBLIC KEY BLOCK-----"); //$NON-NLS-1$ //$NON-NLS-2$
 	}
 
-	private Map<Long, PGPPublicKey> readPublicKeys(String armoredPublicKeyring) {
+	private static Map<Long, PGPPublicKey> readPublicKeys(String armoredPublicKeyring) {
 		if (armoredPublicKeyring == null) {
 			return Collections.emptyMap();
 		}
@@ -154,7 +165,7 @@
 	}
 
 	@Override
-	public void write(int b) throws IOException {
+	public void write(int b) {
 		if (signaturesToVerify != null) {
 			signaturesToVerify.iterator().forEachRemaining(signature -> signature.update((byte) b));
 		}
@@ -200,4 +211,21 @@
 		}
 		setStatus(Status.OK_STATUS);
 	}
+
+	public static Collection<PGPPublicKey> getSigners(IArtifactDescriptor artifact) {
+		try {
+			return getSignatures(artifact).stream() //
+					.mapToLong(PGPSignature::getKeyID) //
+					.mapToObj(Long::valueOf) //
+					.map(knownKeys::get) //
+					.filter(Objects::nonNull).collect(Collectors.toSet());
+		} catch (IOException | PGPException e) {
+			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, e.getMessage(), e));
+			return Collections.emptyList();
+		}
+	}
+
+	public static void discardKnownKeys() {
+		knownKeys.clear();
+	}
 }
diff --git a/bundles/org.eclipse.equinox.p2.artifact.repository/src/org/eclipse/equinox/internal/p2/artifact/repository/MirrorRequest.java b/bundles/org.eclipse.equinox.p2.artifact.repository/src/org/eclipse/equinox/internal/p2/artifact/repository/MirrorRequest.java
index 965277d..d9f4dfe 100644
--- a/bundles/org.eclipse.equinox.p2.artifact.repository/src/org/eclipse/equinox/internal/p2/artifact/repository/MirrorRequest.java
+++ b/bundles/org.eclipse.equinox.p2.artifact.repository/src/org/eclipse/equinox/internal/p2/artifact/repository/MirrorRequest.java
@@ -23,6 +23,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import org.eclipse.core.runtime.*;
+import org.eclipse.equinox.internal.p2.artifact.processors.pgp.PGPSignatureVerifier;
 import org.eclipse.equinox.internal.p2.artifact.repository.simple.SimpleArtifactDescriptor;
 import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
 import org.eclipse.equinox.internal.p2.repository.Transport;
@@ -151,7 +152,7 @@
 			return;
 		}
 
-		IArtifactDescriptor destinationDescriptor = getDestinationDescriptor(descriptor);
+		IArtifactDescriptor destinationDescriptor = getDestinationDescriptor(descriptor, descriptor == canonical);
 		IStatus status = transfer(destinationDescriptor, descriptor, monitor);
 		// if ok, cancelled or transfer has already been done with the canonical form return with status set
 		if (status.getSeverity() == IStatus.CANCEL) {
@@ -176,7 +177,7 @@
 			return;
 		}
 
-		IStatus canonicalStatus = transfer(getDestinationDescriptor(canonical), canonical, monitor);
+		IStatus canonicalStatus = transfer(getDestinationDescriptor(canonical, true), canonical, monitor);
 		// To prevent the optimized transfer status severity from dominating the canonical, only merge
 		// if the canonical severity is equal to or higher than the optimized transfer severity.
 		if (canonicalStatus.getSeverity() < status.getSeverity())
@@ -185,7 +186,7 @@
 			setResult(new MultiStatus(Activator.ID, canonicalStatus.getCode() != 0 ? canonicalStatus.getCode() : status.getCode(), new IStatus[] {status, canonicalStatus}, Messages.MirrorRequest_multipleDownloadProblems, null));
 	}
 
-	private IArtifactDescriptor getDestinationDescriptor(IArtifactDescriptor sourceDescriptor) {
+	private IArtifactDescriptor getDestinationDescriptor(IArtifactDescriptor sourceDescriptor, boolean isCanonical) {
 		// Get the descriptor to use to store the artifact
 		// Since we are mirroring, ensure we clear out data from the original descriptor that may
 		// not apply in the new repo location.
@@ -200,6 +201,14 @@
 			((ArtifactDescriptor) destinationDescriptor).addProperties(targetDescriptorProperties);
 		if (targetRepositoryProperties != null && destinationDescriptor instanceof SimpleArtifactDescriptor)
 			((SimpleArtifactDescriptor) destinationDescriptor).addRepositoryProperties(targetRepositoryProperties);
+		if (isCanonical && destinationDescriptor instanceof ArtifactDescriptor) {
+			// keep some safety properties
+			if (sourceDescriptor.getProperty(PGPSignatureVerifier.PGP_SIGNATURES_PROPERTY_NAME) != null) {
+				((ArtifactDescriptor) destinationDescriptor)
+						.addProperties(Map.of(PGPSignatureVerifier.PGP_SIGNATURES_PROPERTY_NAME,
+								sourceDescriptor.getProperty(PGPSignatureVerifier.PGP_SIGNATURES_PROPERTY_NAME)));
+			}
+		}
 		return destinationDescriptor;
 	}
 
diff --git a/bundles/org.eclipse.equinox.p2.core/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.p2.core/META-INF/MANIFEST.MF
index d8b8128..19cf0c2 100644
--- a/bundles/org.eclipse.equinox.p2.core/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.p2.core/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.equinox.p2.core;singleton:=true
-Bundle-Version: 2.7.0.qualifier
+Bundle-Version: 2.8.0.qualifier
 Bundle-ClassPath: .
 Bundle-Activator: org.eclipse.equinox.internal.p2.core.Activator
 Bundle-Vendor: %providerName
@@ -64,13 +64,15 @@
    org.eclipse.equinox.p2.updatesite,
    org.eclipse.equinox.p2.director.app,
    org.eclipse.equinox.p2.transport.ecf",
- org.eclipse.equinox.p2.core;version="2.0.0",
+ org.eclipse.equinox.p2.core;version="2.7.0",
  org.eclipse.equinox.p2.core.spi;version="2.1.0"
 Require-Bundle: org.eclipse.equinox.common;bundle-version="[3.5.0,4.0.0)"
 Bundle-RequiredExecutionEnvironment: JavaSE-11
 Bundle-ActivationPolicy: lazy
 Service-Component: OSGI-INF/eventBus.xml, OSGI-INF/agentProvider.xml
-Import-Package: org.eclipse.osgi.framework.eventmgr;version="1.2.0",
+Import-Package: org.bouncycastle.bcpg;version="1.65.0",
+ org.bouncycastle.openpgp;version="1.65.0",
+ org.eclipse.osgi.framework.eventmgr;version="1.2.0",
  org.eclipse.osgi.framework.log;version="1.0.0",
  org.eclipse.osgi.service.debug;version="1.0.0",
  org.eclipse.osgi.util;version="1.0.0",
diff --git a/bundles/org.eclipse.equinox.p2.core/pom.xml b/bundles/org.eclipse.equinox.p2.core/pom.xml
index 34dd2a2..3cac38c 100644
--- a/bundles/org.eclipse.equinox.p2.core/pom.xml
+++ b/bundles/org.eclipse.equinox.p2.core/pom.xml
@@ -9,6 +9,6 @@
   </parent>
   <groupId>org.eclipse.equinox</groupId>
   <artifactId>org.eclipse.equinox.p2.core</artifactId>
-  <version>2.7.0-SNAPSHOT</version>
+  <version>2.8.0-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 </project>
diff --git a/bundles/org.eclipse.equinox.p2.core/src/org/eclipse/equinox/p2/core/UIServices.java b/bundles/org.eclipse.equinox.p2.core/src/org/eclipse/equinox/p2/core/UIServices.java
index 2e6e2d9..f2e27a2 100644
--- a/bundles/org.eclipse.equinox.p2.core/src/org/eclipse/equinox/p2/core/UIServices.java
+++ b/bundles/org.eclipse.equinox.p2.core/src/org/eclipse/equinox/p2/core/UIServices.java
@@ -15,6 +15,9 @@
 package org.eclipse.equinox.p2.core;
 
 import java.security.cert.Certificate;
+import java.util.Collection;
+import java.util.Collections;
+import org.bouncycastle.openpgp.PGPPublicKey;
 
 /**
  * Service used for prompting for user information from within lower level code.
@@ -74,11 +77,30 @@
 	 */
 	public static class TrustInfo {
 		private final Certificate[] trustedCertificates;
+		private final Collection<PGPPublicKey> trustedPGPKeys;
 		private final boolean saveTrustedCertificates;
 		private final boolean trustUnsigned;
 
 		public TrustInfo(Certificate[] trusted, boolean save, boolean trustUnsigned) {
 			this.trustedCertificates = trusted;
+			this.trustedPGPKeys = Collections.emptyList();
+			this.saveTrustedCertificates = save;
+			this.trustUnsigned = trustUnsigned;
+		}
+
+		/**
+		 *
+		 * @param trusted
+		 * @param trustedPGPKeys
+		 * @param save
+		 * @param trustUnsigned
+		 * @since 2.8
+		 */
+		public TrustInfo(Collection<Certificate> trustedCertificates, Collection<PGPPublicKey> trustedPGPKeys,
+				boolean save,
+				boolean trustUnsigned) {
+			this.trustedCertificates = trustedCertificates.toArray(Certificate[]::new);
+			this.trustedPGPKeys = trustedPGPKeys;
 			this.saveTrustedCertificates = save;
 			this.trustUnsigned = trustUnsigned;
 		}
@@ -95,6 +117,15 @@
 		}
 
 		/**
+		 *
+		 * @return the trusted PGP keys
+		 * @since 2.8
+		 */
+		public Collection<PGPPublicKey> getTrustedPGPKeys() {
+			return trustedPGPKeys;
+		}
+
+		/**
 		 * Return a boolean indicating whether the trusted certificates should
 		 * be persisted for future operations.
 		 *
@@ -160,4 +191,24 @@
 	public void showInformationMessage(String title, String text, String linkText) {
 		System.out.println(text);
 	}
+
+	/**
+	 * Opens a UI prompt to capture information about trusted content.
+	 *
+	 * @param untrustedChain   - an array of certificate chains for which there is
+	 *                         no current trust anchor. May be <code>null</code>,
+	 *                         which means there are no untrusted certificate
+	 *                         chains.
+	 * @param untrustedPGPKeys
+	 * @param unsignedDetail   - an array of strings, where each String describes
+	 *                         content that is not signed. May be <code>null</code>,
+	 *                         which means there is no unsigned content
+	 * @return the TrustInfo that describes the user's choices for trusting
+	 *         certificates and unsigned content.
+	 * @since 2.8
+	 */
+	public TrustInfo getTrustInfo(Certificate[][] unTrustedCertificateChains, Collection<PGPPublicKey> untrustedPGPKeys,
+			String[] details) {
+		return getTrustInfo(unTrustedCertificateChains, details);
+	}
 }
diff --git a/bundles/org.eclipse.equinox.p2.engine/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.p2.engine/META-INF/MANIFEST.MF
index fdc9583..83eacd2 100644
--- a/bundles/org.eclipse.equinox.p2.engine/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.p2.engine/META-INF/MANIFEST.MF
@@ -14,7 +14,7 @@
    org.eclipse.equinox.p2.ui.sdk.scheduler,
    org.eclipse.pde.build,
    org.eclipse.equinox.p2.director.app",
- org.eclipse.equinox.internal.p2.engine.phases;x-friends:="org.eclipse.equinox.p2.director.app,org.eclipse.equinox.p2.repository.tools,org.eclipse.equinox.p2.ui.sdk.scheduler",
+ org.eclipse.equinox.internal.p2.engine.phases;x-friends:="org.eclipse.equinox.p2.director.app,org.eclipse.equinox.p2.repository.tools,org.eclipse.equinox.p2.ui.sdk.scheduler,org.eclipse.equinox.p2.touchpoint.eclipse",
  org.eclipse.equinox.p2.engine;version="2.2.0",
  org.eclipse.equinox.p2.engine.query;version="2.0.0",
  org.eclipse.equinox.p2.engine.spi;version="2.0.0"
@@ -26,8 +26,10 @@
 Bundle-ActivationPolicy: lazy
 Service-Component: OSGI-INF/profileRegistry.xml, OSGI-INF/engine.xml
 Import-Package: javax.xml.parsers,
+ org.bouncycastle.openpgp;version="1.65.0",
  org.eclipse.core.internal.preferences,
  org.eclipse.core.runtime.preferences,
+ org.eclipse.equinox.internal.p2.artifact.processors.pgp,
  org.eclipse.equinox.internal.p2.core.helpers,
  org.eclipse.equinox.internal.p2.metadata,
  org.eclipse.equinox.internal.p2.metadata.index,
diff --git a/bundles/org.eclipse.equinox.p2.engine/src/org/eclipse/equinox/internal/p2/engine/InstallableUnitOperand.java b/bundles/org.eclipse.equinox.p2.engine/src/org/eclipse/equinox/internal/p2/engine/InstallableUnitOperand.java
index 6fea6e3..8b91ed3 100644
--- a/bundles/org.eclipse.equinox.p2.engine/src/org/eclipse/equinox/internal/p2/engine/InstallableUnitOperand.java
+++ b/bundles/org.eclipse.equinox.p2.engine/src/org/eclipse/equinox/internal/p2/engine/InstallableUnitOperand.java
@@ -27,7 +27,7 @@
 	 * Creates a new operand that represents replacing an installable unit
 	 * with another. At least one of the provided installable units must be
 	 * non-null.
-	 * 
+	 *
 	 * @param first The installable unit being removed, or <code>null</code>
 	 * @param second The installable unit being added, or <code>null</code>
 	 */
@@ -38,10 +38,17 @@
 		this.second = second;
 	}
 
+	/**
+	 *
+	 * @return The installable unit being removed, or <code>null</code>
+	 */
 	public IInstallableUnit first() {
 		return first;
 	}
 
+	/**
+	 * @return The installable unit being added, or <code>null</code>
+	 */
 	public IInstallableUnit second() {
 		return second;
 	}
diff --git a/bundles/org.eclipse.equinox.p2.engine/src/org/eclipse/equinox/internal/p2/engine/phases/CertificateChecker.java b/bundles/org.eclipse.equinox.p2.engine/src/org/eclipse/equinox/internal/p2/engine/phases/CertificateChecker.java
index 7e1e729..964072c 100644
--- a/bundles/org.eclipse.equinox.p2.engine/src/org/eclipse/equinox/internal/p2/engine/phases/CertificateChecker.java
+++ b/bundles/org.eclipse.equinox.p2.engine/src/org/eclipse/equinox/internal/p2/engine/phases/CertificateChecker.java
@@ -18,12 +18,17 @@
 import java.security.GeneralSecurityException;
 import java.security.cert.Certificate;
 import java.util.*;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+import org.bouncycastle.openpgp.PGPPublicKey;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Status;
+import org.eclipse.equinox.internal.p2.artifact.processors.pgp.PGPSignatureVerifier;
 import org.eclipse.equinox.internal.p2.engine.*;
 import org.eclipse.equinox.p2.core.IProvisioningAgent;
 import org.eclipse.equinox.p2.core.UIServices;
 import org.eclipse.equinox.p2.core.UIServices.TrustInfo;
+import org.eclipse.equinox.p2.repository.artifact.IArtifactDescriptor;
 import org.eclipse.osgi.service.security.TrustEngine;
 import org.eclipse.osgi.signedcontent.*;
 import org.eclipse.osgi.util.NLS;
@@ -32,13 +37,17 @@
 import org.osgi.util.tracker.ServiceTracker;
 
 /**
- * Checks the certificates on a set of files or artifacts and reports back any problems
- * with unsigned artifacts, untrusted certificates, or tampered content.
+ * Checks the certificates or PGP signatures on a set of files or artifacts and
+ * reports back any problems with unsigned artifacts, untrusted certificates, or
+ * tampered content.
  */
 public class CertificateChecker {
 	private static final String DEBUG_PREFIX = "certificate checker"; //$NON-NLS-1$
 
-	private ArrayList<File> artifacts;
+	/**
+	 * Stores artifacts to check
+	 */
+	private Map<IArtifactDescriptor, File> artifacts = new HashMap<>();
 	private final IProvisioningAgent agent;
 
 	public CertificateChecker() {
@@ -47,7 +56,28 @@
 
 	public CertificateChecker(IProvisioningAgent agent) {
 		this.agent = agent;
-		artifacts = new ArrayList<>();
+		artifacts = new HashMap<>();
+	}
+
+	private static class PGPPublicKeyEntry {
+		public final PGPPublicKey key;
+
+		public PGPPublicKeyEntry(PGPPublicKey key) {
+			this.key = key;
+		}
+
+		@Override
+		public boolean equals(Object obj) {
+			if (!(obj instanceof PGPPublicKeyEntry)) {
+				return false;
+			}
+			return key.getKeyID() == ((PGPPublicKeyEntry) obj).key.getKeyID();
+		}
+
+		@Override
+		public int hashCode() {
+			return Long.hashCode(key.getKeyID());
+		}
 	}
 
 	public IStatus start() {
@@ -63,66 +93,70 @@
 
 	private IStatus checkCertificates(SignedContentFactory verifierFactory) {
 		UIServices serviceUI = agent.getService(UIServices.class);
-		SignedContent content = null;
-		SignerInfo[] signerInfo = null;
-		ArrayList<Certificate> untrusted = new ArrayList<>();
-		ArrayList<File> unsigned = new ArrayList<>();
+		ArrayList<Certificate> untrustedCertificates = new ArrayList<>();
+		Map<IArtifactDescriptor, Collection<PGPPublicKey>> untrustedPGPArtifacts = new HashMap<>();
+		Map<PGPPublicKeyEntry, Collection<IArtifactDescriptor>> untrustedPGPKeys = new HashMap<>();
+		Map<IArtifactDescriptor, File> unsigned = new HashMap<>();
 		ArrayList<Certificate[]> untrustedChain = new ArrayList<>();
 		Map<Certificate, Collection<File>> untrustedArtifacts = new HashMap<>();
 		IStatus status = Status.OK_STATUS;
-		if (artifacts.size() == 0 || serviceUI == null)
+		if (artifacts.isEmpty() || serviceUI == null) {
 			return status;
-		checkArtifacts: for (File artifact : artifacts) {
+		}
+		for (Entry<IArtifactDescriptor, File> artifact : artifacts.entrySet()) {
+			File artifactFile = artifact.getValue();
 			try {
-				content = verifierFactory.getSignedContent(artifact);
-				if (!content.isSigned()) {
-					unsigned.add(artifact);
-					continue;
+				SignedContent content = verifierFactory.getSignedContent(artifactFile);
+				if (content.isSigned()) {
+					SignerInfo[] signerInfo = content.getSignerInfos();
+					if (Arrays.stream(signerInfo).noneMatch(SignerInfo::isTrusted)) {
+						// Only record the untrusted elements if there are no trusted elements.
+						for (SignerInfo element : signerInfo) {
+							if (!element.isTrusted()) {
+								Certificate[] certificateChain = element.getCertificateChain();
+								if (!untrustedCertificates.contains(certificateChain[0])) {
+									untrustedCertificates.add(certificateChain[0]);
+									untrustedChain.add(certificateChain);
+								}
+								if (DebugHelper.DEBUG_CERTIFICATE_CHECKER_UNTRUSTED) {
+									untrustedArtifacts.computeIfAbsent(certificateChain[0], key -> new ArrayList<>())
+											.add(artifactFile);
+								}
+							}
+						}
+					}
+				} else {
+					Collection<PGPPublicKey> signers = PGPSignatureVerifier.getSigners(artifact.getKey());
+					if (!signers.isEmpty()) {
+						if (signers.stream().noneMatch(this::isTrusted)) {
+							untrustedPGPArtifacts.putIfAbsent(artifact.getKey(), signers);
+							signers.forEach(signer -> untrustedPGPKeys
+									.computeIfAbsent(new PGPPublicKeyEntry(signer), key -> new HashSet<>())
+									.add(artifact.getKey()));
+						}
+					} else {
+						unsigned.put(artifact.getKey(), artifactFile);
+					}
 				}
-				signerInfo = content.getSignerInfos();
+
 			} catch (GeneralSecurityException e) {
 				return new Status(IStatus.ERROR, EngineActivator.ID, Messages.CertificateChecker_SignedContentError, e);
 			} catch (IOException e) {
 				return new Status(IStatus.ERROR, EngineActivator.ID, Messages.CertificateChecker_SignedContentIOError, e);
 			}
-
-			// Determine if any element is trusted.
-			for (SignerInfo element : signerInfo) {
-				if (element.isTrusted()) {
-					continue checkArtifacts;
-				}
-			}
-
-			// Only record the untrusted elements if there are no trusted elements.
-			for (SignerInfo element : signerInfo) {
-				if (!element.isTrusted()) {
-					Certificate[] certificateChain = element.getCertificateChain();
-					if (!untrusted.contains(certificateChain[0])) {
-						untrusted.add(certificateChain[0]);
-						untrustedChain.add(certificateChain);
-					}
-					if (DebugHelper.DEBUG_CERTIFICATE_CHECKER_UNTRUSTED) {
-						if (untrustedArtifacts.containsKey(certificateChain[0])) {
-							untrustedArtifacts.get(certificateChain[0]).add(artifact);
-						} else {
-							untrustedArtifacts.put(certificateChain[0], new ArrayList<>(Arrays.asList(artifact)));
-						}
-					}
-				}
-			}
 		}
 
 		// log the unsigned artifacts if requested
 		if (DebugHelper.DEBUG_CERTIFICATE_CHECKER_UNSIGNED && !unsigned.isEmpty()) {
 			StringBuilder message = new StringBuilder("The following artifacts are unsigned:\n"); //$NON-NLS-1$
-			for (File file : unsigned) {
+			for (File file : unsigned.values()) {
 				message.append(NLS.bind("  {0}\n", file.getPath())); //$NON-NLS-1$
 			}
 			DebugHelper.debug(DEBUG_PREFIX, message.toString());
 		}
 
 		// log the untrusted certificates if requested
-		if (DebugHelper.DEBUG_CERTIFICATE_CHECKER_UNTRUSTED && !untrusted.isEmpty()) {
+		if (DebugHelper.DEBUG_CERTIFICATE_CHECKER_UNTRUSTED && !untrustedCertificates.isEmpty()) {
 			StringBuilder message = new StringBuilder("The following certificates are untrusted:\n"); //$NON-NLS-1$
 			for (Certificate cert : untrustedArtifacts.keySet()) {
 				message.append(cert.toString() + "\n"); //$NON-NLS-1$
@@ -133,41 +167,41 @@
 			}
 			DebugHelper.debug(DEBUG_PREFIX, message.toString());
 		}
+		if (DebugHelper.DEBUG_CERTIFICATE_CHECKER_UNTRUSTED && !untrustedPGPKeys.isEmpty()) {
+			StringBuilder message = new StringBuilder("The following PGP Keys are untrusted:\n"); //$NON-NLS-1$
+			for (Entry<PGPPublicKeyEntry, Collection<IArtifactDescriptor>> entry : untrustedPGPKeys.entrySet()) {
+				message.append(entry.getKey().key.getKeyID() + "\n"); //$NON-NLS-1$
+				message.append("  used by the following artifacts:\n"); //$NON-NLS-1$
+				for (IArtifactDescriptor artifact : entry.getValue()) {
+					message.append(NLS.bind("    {0}\n", artifact.getArtifactKey())); //$NON-NLS-1$
+				}
+			}
+			DebugHelper.debug(DEBUG_PREFIX, message.toString());
+		}
 
 		String policy = getUnsignedContentPolicy();
 		//if there is unsigned content and we should never allow it, then fail without further checking certificates
-		if (!unsigned.isEmpty() && EngineActivator.UNSIGNED_FAIL.equals(policy))
+		if (!unsigned.isEmpty() && EngineActivator.UNSIGNED_FAIL.equals(policy)) {
 			return new Status(IStatus.ERROR, EngineActivator.ID, NLS.bind(Messages.CertificateChecker_UnsignedNotAllowed, unsigned));
-
-		String[] details;
-		// If we always allow unsigned content, or we don't have any, we don't prompt the user about it
-		if (EngineActivator.UNSIGNED_ALLOW.equals(policy) || unsigned.isEmpty())
-			details = null;
-		else {
-			details = new String[unsigned.size()];
-			for (int i = 0; i < details.length; i++) {
-				details[i] = unsigned.get(i).toString();
-			}
-		}
-		Certificate[][] unTrustedCertificateChains;
-		if (untrusted.isEmpty()) {
-			unTrustedCertificateChains = null;
-		} else {
-			unTrustedCertificateChains = new Certificate[untrustedChain.size()][];
-			for (int i = 0; i < untrustedChain.size(); i++) {
-				unTrustedCertificateChains[i] = untrustedChain.get(i);
-			}
 		}
 
+		String[] details = EngineActivator.UNSIGNED_ALLOW.equals(policy) || unsigned.isEmpty() ? null
+				: unsigned.values().stream().map(Object::toString).toArray(String[]::new);
+		Certificate[][] unTrustedCertificateChains = untrustedCertificates.isEmpty() ? null
+				: untrustedChain.toArray(Certificate[][]::new);
 		// If there was no unsigned content, and nothing untrusted, no need to prompt.
-		if (details == null && unTrustedCertificateChains == null)
+		if (details == null && unTrustedCertificateChains == null && untrustedPGPArtifacts.isEmpty()) {
 			return status;
+		}
 
-		TrustInfo trustInfo = serviceUI.getTrustInfo(unTrustedCertificateChains, details);
+		TrustInfo trustInfo = serviceUI.getTrustInfo(unTrustedCertificateChains,
+				untrustedPGPKeys.keySet().stream().map(entry -> entry.key).collect(Collectors.toUnmodifiableList()),
+				details);
 
 		// If user doesn't trust unsigned content, cancel the operation
-		if (!trustInfo.trustUnsignedContent())
+		if (!unsigned.isEmpty() && !trustInfo.trustUnsignedContent()) {
 			return Status.CANCEL_STATUS;
+		}
 
 		Certificate[] trustedCertificates = trustInfo.getTrustedCertificates();
 		// If we had untrusted chains and nothing was trusted, cancel the operation
@@ -177,16 +211,22 @@
 		// Anything that was trusted should be removed from the untrusted list
 		if (trustedCertificates != null) {
 			for (Certificate trustedCertificate : trustedCertificates) {
-				untrusted.remove(trustedCertificate);
+				untrustedCertificates.remove(trustedCertificate);
 			}
 		}
+		Collection<PGPPublicKey> trustedPGPKeys = trustInfo.getTrustedPGPKeys();
+		untrustedPGPArtifacts.values().removeIf(pgpKeys -> !Collections.disjoint(pgpKeys, trustedPGPKeys));
+		trustedPGPKeys.stream().map(PGPPublicKeyEntry::new).forEach(untrustedPGPKeys::remove);
 
 		// If there is still untrusted content, cancel the operation
-		if (untrusted.size() > 0)
+		if (!untrustedCertificates.isEmpty() || !untrustedPGPKeys.isEmpty()) {
 			return new Status(IStatus.CANCEL, EngineActivator.ID, Messages.CertificateChecker_CertificateRejected);
+		}
 		// If we should persist the trusted certificates, add them to the trust engine
-		if (trustInfo.persistTrust())
+		if (trustInfo.persistTrust()) {
 			return persistTrustedCertificates(trustedCertificates);
+			// do not persist PGP key at the moment
+		}
 
 		return status;
 	}
@@ -235,14 +275,12 @@
 
 	}
 
-	public void add(File toAdd) {
-		artifacts.add(toAdd);
+	public void add(Map<IArtifactDescriptor, File> toAdd) {
+		artifacts.putAll(toAdd);
 	}
 
-	public void add(Object[] toAdd) {
-		for (Object element : toAdd) {
-			if (element instanceof File)
-				add((File) element);
-		}
+	private boolean isTrusted(PGPPublicKey pgppublickey) {
+		return false;
 	}
 }
+
diff --git a/bundles/org.eclipse.equinox.p2.engine/src/org/eclipse/equinox/internal/p2/engine/phases/CheckTrust.java b/bundles/org.eclipse.equinox.p2.engine/src/org/eclipse/equinox/internal/p2/engine/phases/CheckTrust.java
index 531f953..e885ad1 100644
--- a/bundles/org.eclipse.equinox.p2.engine/src/org/eclipse/equinox/internal/p2/engine/phases/CheckTrust.java
+++ b/bundles/org.eclipse.equinox.p2.engine/src/org/eclipse/equinox/internal/p2/engine/phases/CheckTrust.java
@@ -7,7 +7,7 @@
  *  https://www.eclipse.org/legal/epl-2.0/
  *
  *  SPDX-License-Identifier: EPL-2.0
- * 
+ *
  *  Contributors:
  *     IBM Corporation - initial API and implementation
  *******************************************************************************/
@@ -25,6 +25,7 @@
 import org.eclipse.equinox.p2.engine.spi.ProvisioningAction;
 import org.eclipse.equinox.p2.metadata.IInstallableUnit;
 import org.eclipse.equinox.p2.metadata.ITouchpointType;
+import org.eclipse.equinox.p2.repository.artifact.IArtifactDescriptor;
 
 /**
  * An install phase that checks if the certificates used to sign the artifacts
@@ -32,7 +33,13 @@
  */
 public class CheckTrust extends InstallableUnitPhase {
 
-	public static final String PARM_ARTIFACT_FILES = "artifactFiles"; //$NON-NLS-1$
+	/**
+	 * Parameter used to populate/get artifacts to check trust. The value for this
+	 * property is <code>Map&lt;IArtifactDescriptor, File></code>.
+	 *
+	 * @see org.eclipse.equinox.internal.p2.touchpoint.eclipse.actions.CheckTrustAction
+	 */
+	public static final String PARM_ARTIFACTS = "artifacts"; //$NON-NLS-1$
 
 	public CheckTrust(int weight) {
 		super(PhaseSetFactory.PHASE_CHECK_TRUST, weight);
@@ -46,15 +53,14 @@
 	@Override
 	protected IStatus completePhase(IProgressMonitor monitor, IProfile profile, Map<String, Object> parameters) {
 		@SuppressWarnings("unchecked")
-		Collection<File> artifactRequests = (Collection<File>) parameters.get(PARM_ARTIFACT_FILES);
+		Map<IArtifactDescriptor, File> artifactRequests = (Map<IArtifactDescriptor, File>) parameters
+				.get(PARM_ARTIFACTS);
 		IProvisioningAgent agent = (IProvisioningAgent) parameters.get(PARM_AGENT);
 
 		// Instantiate a check trust manager
 		CertificateChecker certificateChecker = new CertificateChecker(agent);
-		certificateChecker.add(artifactRequests.toArray());
-		IStatus status = certificateChecker.start();
-
-		return status;
+		certificateChecker.add(artifactRequests);
+		return certificateChecker.start();
 	}
 
 	@Override
@@ -86,7 +92,7 @@
 
 	@Override
 	protected IStatus initializePhase(IProgressMonitor monitor, IProfile profile, Map<String, Object> parameters) {
-		parameters.put(PARM_ARTIFACT_FILES, new ArrayList<File>());
+		parameters.put(PARM_ARTIFACTS, new HashMap<IArtifactDescriptor, File>());
 		return super.initializePhase(monitor, profile, parameters);
 	}
 
diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/artifact/repository/PGPTest.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/artifact/repository/PGPTest.java
index bd2e3e2..595a623 100644
--- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/artifact/repository/PGPTest.java
+++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/artifact/repository/PGPTest.java
@@ -13,17 +13,26 @@
 import java.nio.file.Files;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.equinox.internal.p2.artifact.processors.pgp.PGPSignatureVerifier;
 import org.eclipse.equinox.internal.p2.artifact.repository.MirrorRequest;
 import org.eclipse.equinox.internal.p2.metadata.ArtifactKey;
 import org.eclipse.equinox.p2.metadata.Version;
 import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
 import org.eclipse.equinox.p2.tests.AbstractProvisioningTest;
+import org.junit.Before;
 import org.junit.Test;
 
 public class PGPTest extends AbstractProvisioningTest {
 	IArtifactRepository targetRepo = null;
 	IArtifactRepository sourceRepo = null;
 
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		PGPSignatureVerifier.discardKnownKeys();
+	}
+
 	private void loadPGPTestRepo(String repoName) throws Exception {
 		sourceRepo = getArtifactRepositoryManager().loadRepository(
 				getTestData("Test repository for PGP", "testData/pgp/" + repoName).toURI(), new NullProgressMonitor());
diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/engine/CertificateCheckerTest.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/engine/CertificateCheckerTest.java
index 79c58fa..dd7cc84 100644
--- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/engine/CertificateCheckerTest.java
+++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/engine/CertificateCheckerTest.java
@@ -16,11 +16,15 @@
 import java.io.File;
 import java.io.IOException;
 import java.security.cert.Certificate;
+import java.util.Map;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.equinox.internal.p2.core.ProvisioningAgent;
 import org.eclipse.equinox.internal.p2.engine.EngineActivator;
 import org.eclipse.equinox.internal.p2.engine.phases.CertificateChecker;
+import org.eclipse.equinox.internal.p2.metadata.ArtifactKey;
 import org.eclipse.equinox.p2.core.UIServices;
+import org.eclipse.equinox.p2.metadata.Version;
+import org.eclipse.equinox.p2.repository.artifact.spi.ArtifactDescriptor;
 import org.eclipse.equinox.p2.tests.AbstractProvisioningTest;
 import org.eclipse.equinox.p2.tests.TestActivator;
 import org.eclipse.equinox.p2.tests.TestData;
@@ -80,7 +84,7 @@
 			//if the service is consulted it will say no
 			serviceUI.unsignedReturnValue = false;
 			System.getProperties().setProperty(EngineActivator.PROP_UNSIGNED_POLICY, EngineActivator.UNSIGNED_ALLOW);
-			checker.add(unsigned);
+			checker.add(Map.of(new ArtifactDescriptor(new ArtifactKey("what", "ever", Version.create("1"))), unsigned));
 			IStatus result = checker.start();
 			assertEquals("1.0", IStatus.OK, result.getSeverity());
 		} finally {
@@ -94,7 +98,7 @@
 	public void testPolicyFail() {
 		try {
 			System.getProperties().setProperty(EngineActivator.PROP_UNSIGNED_POLICY, EngineActivator.UNSIGNED_FAIL);
-			checker.add(unsigned);
+			checker.add(Map.of(new ArtifactDescriptor(new ArtifactKey("what", "ever", Version.create("1"))), unsigned));
 			IStatus result = checker.start();
 			assertEquals("1.0", IStatus.ERROR, result.getSeverity());
 
@@ -110,7 +114,7 @@
 		try {
 			System.getProperties().setProperty(EngineActivator.PROP_UNSIGNED_POLICY, EngineActivator.UNSIGNED_PROMPT);
 			serviceUI.unsignedReturnValue = true;
-			checker.add(unsigned);
+			checker.add(Map.of(new ArtifactDescriptor(new ArtifactKey("what", "ever", Version.create("1"))), unsigned));
 			IStatus result = checker.start();
 			assertEquals("1.0", IStatus.OK, result.getSeverity());
 			assertTrue("1.1", serviceUI.wasPrompted);
@@ -125,7 +129,7 @@
 	public void testPolicyDefault() {
 		System.getProperties().remove(EngineActivator.PROP_UNSIGNED_POLICY);
 		serviceUI.unsignedReturnValue = true;
-		checker.add(unsigned);
+		checker.add(Map.of(new ArtifactDescriptor(new ArtifactKey("what", "ever", Version.create("1"))), unsigned));
 		IStatus result = checker.start();
 		assertEquals("1.0", IStatus.OK, result.getSeverity());
 		assertTrue("1.1", serviceUI.wasPrompted);
@@ -138,7 +142,7 @@
 		try {
 			System.getProperties().setProperty(EngineActivator.PROP_UNSIGNED_POLICY, EngineActivator.UNSIGNED_PROMPT);
 			serviceUI.unsignedReturnValue = false;
-			checker.add(unsigned);
+			checker.add(Map.of(new ArtifactDescriptor(new ArtifactKey("what", "ever", Version.create("1"))), unsigned));
 			IStatus result = checker.start();
 			assertEquals("1.0", IStatus.CANCEL, result.getSeverity());
 			assertTrue("1.1", serviceUI.wasPrompted);
@@ -156,7 +160,7 @@
 		try {
 			// Intentionally replace our service with a null service
 			testAgent.registerService(UIServices.SERVICE_NAME, null);
-			checker.add(unsigned);
+			checker.add(Map.of(new ArtifactDescriptor(new ArtifactKey("what", "ever", Version.create("1"))), unsigned));
 			// TODO need to add some untrusted files here, too.  To prove that we treated them as trusted temporarily
 			System.getProperties().setProperty(EngineActivator.PROP_UNSIGNED_POLICY, EngineActivator.UNSIGNED_PROMPT);
 			IStatus result = checker.start();
@@ -165,4 +169,5 @@
 			System.getProperties().remove(EngineActivator.PROP_UNSIGNED_POLICY);
 		}
 	}
+
 }
diff --git a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/eclipse/CheckTrustActionTest.java b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/eclipse/CheckTrustActionTest.java
index fbc3fc5..b1cade2 100644
--- a/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/eclipse/CheckTrustActionTest.java
+++ b/bundles/org.eclipse.equinox.p2.tests/src/org/eclipse/equinox/p2/tests/touchpoint/eclipse/CheckTrustActionTest.java
@@ -14,7 +14,9 @@
 package org.eclipse.equinox.p2.tests.touchpoint.eclipse;
 
 import java.io.File;
-import java.util.*;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import org.eclipse.core.runtime.NullProgressMonitor;
 import org.eclipse.equinox.internal.p2.engine.InstallableUnitOperand;
 import org.eclipse.equinox.internal.p2.engine.phases.CheckTrust;
@@ -65,7 +67,7 @@
 		Map<String, Object> parameters = new HashMap<>();
 		parameters.put(ActionConstants.PARM_AGENT, getAgent());
 		parameters.put(ActionConstants.PARM_PROFILE, profile);
-		parameters.put(CheckTrust.PARM_ARTIFACT_FILES, new ArrayList<>());
+		parameters.put(CheckTrust.PARM_ARTIFACTS, new HashMap<>());
 		EclipseTouchpoint touchpoint = new EclipseTouchpoint();
 		touchpoint.initializePhase(null, profile, "test", parameters);
 		InstallableUnitOperand operand = new InstallableUnitOperand(null, iu);
@@ -73,13 +75,13 @@
 		touchpoint.initializeOperand(profile, parameters);
 		parameters = Collections.unmodifiableMap(parameters);
 
-		assertFalse(((List<?>) parameters.get(CheckTrust.PARM_ARTIFACT_FILES)).contains(osgiTarget));
+		assertFalse(((Map<?, File>) parameters.get(CheckTrust.PARM_ARTIFACTS)).values().contains(osgiTarget));
 		CheckTrustAction action = new CheckTrustAction();
 		action.execute(parameters);
-		assertTrue(((List<?>) parameters.get(CheckTrust.PARM_ARTIFACT_FILES)).contains(osgiTarget));
+		assertTrue(((Map<?, File>) parameters.get(CheckTrust.PARM_ARTIFACTS)).values().contains(osgiTarget));
 		// does nothing so should not alter parameters
 		action.undo(parameters);
-		assertTrue(((List<?>) parameters.get(CheckTrust.PARM_ARTIFACT_FILES)).contains(osgiTarget));
+		assertTrue(((Map<?, File>) parameters.get(CheckTrust.PARM_ARTIFACTS)).values().contains(osgiTarget));
 	}
 
 }
\ No newline at end of file
diff --git a/bundles/org.eclipse.equinox.p2.tests/testData/CertificateChecker/selfsigned/artifacts.xml b/bundles/org.eclipse.equinox.p2.tests/testData/CertificateChecker/selfsigned/artifacts.xml
new file mode 100644
index 0000000..68b911b
--- /dev/null
+++ b/bundles/org.eclipse.equinox.p2.tests/testData/CertificateChecker/selfsigned/artifacts.xml
@@ -0,0 +1,16 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<?artifactRepository version='1.1.0'?>
+<repository name='file:/home/mistria/sandbox/repo/ - artifacts' type='org.eclipse.equinox.p2.artifact.repository.simpleRepository' version='1'>
+  <properties size='2'>
+    <property name='p2.timestamp' value='1618433231272'/>
+    <property name='p2.compressed' value='false'/>
+  </properties>
+  <mappings size='3'>
+    <rule filter='(&amp; (classifier=osgi.bundle))' output='${repoUrl}/plugins/${id}_${version}.jar'/>
+    <rule filter='(&amp; (classifier=binary))' output='${repoUrl}/binary/${id}_${version}'/>
+    <rule filter='(&amp; (classifier=org.eclipse.update.feature))' output='${repoUrl}/features/${id}_${version}.jar'/>
+  </mappings>
+  <artifacts size='1'>
+    <artifact classifier='osgi.bundle' id='blah' version='1.0.0.123456'/>
+  </artifacts>
+</repository>
diff --git a/bundles/org.eclipse.equinox.p2.tests/testData/CertificateChecker/selfsigned/content.xml b/bundles/org.eclipse.equinox.p2.tests/testData/CertificateChecker/selfsigned/content.xml
new file mode 100644
index 0000000..db8f67b
--- /dev/null
+++ b/bundles/org.eclipse.equinox.p2.tests/testData/CertificateChecker/selfsigned/content.xml
@@ -0,0 +1,49 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<?metadataRepository version='1.2.0'?>
+<repository name='file:/home/mistria/sandbox/repo/ - metadata' type='org.eclipse.equinox.internal.p2.metadata.repository.LocalMetadataRepository' version='1'>
+  <properties size='2'>
+    <property name='p2.timestamp' value='1618433231272'/>
+    <property name='p2.compressed' value='false'/>
+  </properties>
+  <units size='2'>
+    <unit id='blah' version='1.0.0.123456' singleton='false'>
+      <update id='blah' range='[0.0.0,1.0.0.123456)' severity='0'/>
+      <provides size='4'>
+        <provided namespace='org.eclipse.equinox.p2.iu' name='blah' version='1.0.0.123456'/>
+        <provided namespace='osgi.bundle' name='blah' version='1.0.0.123456'/>
+        <provided namespace='osgi.identity' name='blah' version='1.0.0.123456'>
+          <properties size='1'>
+            <property name='type' value='osgi.bundle'/>
+          </properties>
+        </provided>
+        <provided namespace='org.eclipse.equinox.p2.eclipse.type' name='bundle' version='1.0.0'/>
+      </provides>
+      <artifacts size='1'>
+        <artifact classifier='osgi.bundle' id='blah' version='1.0.0.123456'/>
+      </artifacts>
+      <touchpoint id='org.eclipse.equinox.p2.osgi' version='1.0.0'/>
+      <touchpointData size='1'>
+        <instructions size='2'>
+          <instruction key='zipped'>
+            true
+          </instruction>
+          <instruction key='manifest'>
+            Bundle-SymbolicName: blah&#xA;Bundle-Version: 1.0.0.qualifier&#xA;
+          </instruction>
+        </instructions>
+      </touchpointData>
+    </unit>
+    <unit id='Category' version='0.0.0'>
+      <properties size='3'>
+        <property name='org.eclipse.equinox.p2.type.category' value='true'/>
+      </properties>
+      <provides size='1'>
+        <provided namespace='org.eclipse.equinox.p2.iu' name='Category' version='0.0.0'/>
+      </provides>
+      <requires size='1'>
+        <required namespace='org.eclipse.equinox.p2.iu' name='blah' range='[1.0.0.123456,1.0.0.123456]'/>
+      </requires>
+      <touchpoint id='null' version='0.0.0'/>
+    </unit>
+  </units>
+</repository>
diff --git a/bundles/org.eclipse.equinox.p2.tests/testData/CertificateChecker/selfsigned/plugins/blah_1.0.0.123456.jar b/bundles/org.eclipse.equinox.p2.tests/testData/CertificateChecker/selfsigned/plugins/blah_1.0.0.123456.jar
new file mode 100644
index 0000000..81573b2
--- /dev/null
+++ b/bundles/org.eclipse.equinox.p2.tests/testData/CertificateChecker/selfsigned/plugins/blah_1.0.0.123456.jar
Binary files differ
diff --git a/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/META-INF/MANIFEST.MF
index 5c3932f..71901ce 100644
--- a/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.equinox.p2.touchpoint.eclipse;singleton:=true
-Bundle-Version: 2.3.0.qualifier
+Bundle-Version: 2.3.100.qualifier
 Bundle-Activator: org.eclipse.equinox.internal.p2.touchpoint.eclipse.Activator
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
@@ -19,8 +19,10 @@
 Bundle-ActivationPolicy: lazy
 Import-Package: javax.xml.parsers,
  org.eclipse.equinox.frameworkadmin;version="[2.0.0,3.0.0)",
+ org.eclipse.equinox.internal.p2.artifact.processors.pgp,
  org.eclipse.equinox.internal.p2.core.helpers,
  org.eclipse.equinox.internal.p2.engine,
+ org.eclipse.equinox.internal.p2.engine.phases,
  org.eclipse.equinox.internal.p2.garbagecollector,
  org.eclipse.equinox.internal.p2.metadata,
  org.eclipse.equinox.internal.provisional.frameworkadmin,
diff --git a/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/pom.xml b/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/pom.xml
index 7fc6bae..12d4928 100644
--- a/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/pom.xml
+++ b/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/pom.xml
@@ -9,6 +9,6 @@
   </parent>
   <groupId>org.eclipse.equinox</groupId>
   <artifactId>org.eclipse.equinox.p2.touchpoint.eclipse</artifactId>
-  <version>2.3.0-SNAPSHOT</version>
+  <version>2.3.100-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 </project>
diff --git a/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/src/org/eclipse/equinox/internal/p2/touchpoint/eclipse/actions/ActionConstants.java b/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/src/org/eclipse/equinox/internal/p2/touchpoint/eclipse/actions/ActionConstants.java
index ee394cf..ec918b5 100644
--- a/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/src/org/eclipse/equinox/internal/p2/touchpoint/eclipse/actions/ActionConstants.java
+++ b/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/src/org/eclipse/equinox/internal/p2/touchpoint/eclipse/actions/ActionConstants.java
@@ -17,7 +17,6 @@
 
 	public static final String PARM_AGENT = "agent"; //$NON-NLS-1$
 	public static final String PARM_AT_ARTIFACT = "@artifact"; //$NON-NLS-1$
-	public static final String PARM_ARTIFACT_FILES = "artifactFiles"; //$NON-NLS-1$
 	public static final String PARM_ARTIFACT_REQUESTS = "artifactRequests"; //$NON-NLS-1$
 	public static final String PARM_BUNDLE = "bundle"; //$NON-NLS-1$
 	public static final String PARM_FEATURE = "feature"; //$NON-NLS-1$
diff --git a/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/src/org/eclipse/equinox/internal/p2/touchpoint/eclipse/actions/CheckTrustAction.java b/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/src/org/eclipse/equinox/internal/p2/touchpoint/eclipse/actions/CheckTrustAction.java
index 2015355..7c7b51c 100644
--- a/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/src/org/eclipse/equinox/internal/p2/touchpoint/eclipse/actions/CheckTrustAction.java
+++ b/bundles/org.eclipse.equinox.p2.touchpoint.eclipse/src/org/eclipse/equinox/internal/p2/touchpoint/eclipse/actions/CheckTrustAction.java
@@ -18,6 +18,7 @@
 import java.util.Map;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Status;
+import org.eclipse.equinox.internal.p2.engine.phases.CheckTrust;
 import org.eclipse.equinox.internal.p2.touchpoint.eclipse.EclipseTouchpoint;
 import org.eclipse.equinox.internal.p2.touchpoint.eclipse.Util;
 import org.eclipse.equinox.p2.core.IProvisioningAgent;
@@ -26,6 +27,8 @@
 import org.eclipse.equinox.p2.metadata.IArtifactKey;
 import org.eclipse.equinox.p2.metadata.IInstallableUnit;
 import org.eclipse.equinox.p2.query.QueryUtil;
+import org.eclipse.equinox.p2.repository.artifact.IArtifactDescriptor;
+import org.eclipse.equinox.p2.repository.artifact.IFileArtifactRepository;
 
 /**
  * This action collects the set of bundle files on which the signature trust
@@ -47,14 +50,21 @@
 		if (!profile.available(QueryUtil.createIUQuery(iu), null).isEmpty())
 			return null;
 		@SuppressWarnings("unchecked")
-		Collection<File> bundleFiles = (Collection<File>) parameters.get(ActionConstants.PARM_ARTIFACT_FILES);
+		Map<IArtifactDescriptor, File> bundleFiles = (Map<IArtifactDescriptor, File>) parameters
+				.get(CheckTrust.PARM_ARTIFACTS);
 		Collection<IArtifactKey> artifacts = iu.getArtifacts();
-		if (artifacts == null)
+		if (artifacts == null) {
 			return null;
+		}
+		IFileArtifactRepository repo = Util.getAggregatedBundleRepository(agent, profile);
 		for (IArtifactKey key : artifacts) {
-			File bundleFile = Util.getArtifactFile(agent, key, profile);
-			if (!bundleFiles.contains(bundleFile))
-				bundleFiles.add(bundleFile);
+			for (IArtifactDescriptor descriptor : repo.getArtifactDescriptors(key)) {
+				IFileArtifactRepository currentRepo = descriptor.getRepository() instanceof IFileArtifactRepository
+						? (IFileArtifactRepository) descriptor.getRepository()
+						: repo;
+				File artifactFile = currentRepo.getArtifactFile(descriptor);
+				bundleFiles.put(descriptor, artifactFile);
+			}
 		}
 		return null;
 	}
diff --git a/bundles/org.eclipse.equinox.p2.ui/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.p2.ui/META-INF/MANIFEST.MF
index 270399d..f5f8f38 100644
--- a/bundles/org.eclipse.equinox.p2.ui/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.p2.ui/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %bundleName
 Bundle-SymbolicName: org.eclipse.equinox.p2.ui;singleton:=true
-Bundle-Version: 2.7.100.qualifier
+Bundle-Version: 2.7.200.qualifier
 Bundle-Activator: org.eclipse.equinox.internal.p2.ui.ProvUIActivator
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
@@ -36,6 +36,9 @@
  org.eclipse.equinox.security.ui;bundle-version="[1.0.0,2.0.0)",
  org.eclipse.e4.ui.dialogs;bundle-version="1.1.600"
 Import-Package: javax.xml.parsers,
+ org.bouncycastle.bcpg;version="1.65.0",
+ org.bouncycastle.openpgp;version="1.65.0",
+ org.bouncycastle.util;version="1.65.1",
  org.eclipse.equinox.internal.p2.artifact.repository,
  org.eclipse.equinox.internal.p2.core.helpers,
  org.eclipse.equinox.internal.p2.director,
@@ -46,7 +49,7 @@
  org.eclipse.equinox.internal.provisional.configurator,
  org.eclipse.equinox.internal.provisional.p2.core.eventbus,
  org.eclipse.equinox.internal.provisional.p2.repository,
- org.eclipse.equinox.p2.core;version="[2.0.0,3.0.0)",
+ org.eclipse.equinox.p2.core;version="[2.7.0,3.0.0)",
  org.eclipse.equinox.p2.core.spi;version="[2.0.0,3.0.0)",
  org.eclipse.equinox.p2.engine;version="[2.0.0,3.0.0)",
  org.eclipse.equinox.p2.engine.query;version="[2.0.0,3.0.0)",
diff --git a/bundles/org.eclipse.equinox.p2.ui/pom.xml b/bundles/org.eclipse.equinox.p2.ui/pom.xml
index 5954a5e..770c5b8 100644
--- a/bundles/org.eclipse.equinox.p2.ui/pom.xml
+++ b/bundles/org.eclipse.equinox.p2.ui/pom.xml
@@ -19,6 +19,6 @@
   </parent>
   <groupId>org.eclipse.equinox</groupId>
   <artifactId>org.eclipse.equinox.p2.ui</artifactId>
-  <version>2.7.100-SNAPSHOT</version>
+  <version>2.7.200-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 </project>
diff --git a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/ProvUIMessages.java b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/ProvUIMessages.java
index 07ad9b6..bcee1d0 100644
--- a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/ProvUIMessages.java
+++ b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/ProvUIMessages.java
@@ -277,6 +277,9 @@
 	public static String TrustCertificateDialog_AcceptSelectedButtonLabel;
 	public static String TrustCertificateDialog_SelectAll;
 	public static String TrustCertificateDialog_DeselectAll;
+	public static String TrustCertificateDialog_ObjectType;
+	public static String TrustCertificateDialog_Id;
+	public static String TrustCertificateDialog_Name;
 	// Operations
 	public static String UpdateManagerCompatibility_ExportSitesTitle;
 	public static String UpdateManagerCompatibility_ImportSitesTitle;
diff --git a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/ValidationDialogServiceUI.java b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/ValidationDialogServiceUI.java
index 1c91355..482fe48 100644
--- a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/ValidationDialogServiceUI.java
+++ b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/ValidationDialogServiceUI.java
@@ -16,15 +16,17 @@
 
 import java.net.URL;
 import java.security.cert.Certificate;
+import java.util.*;
+import java.util.List;
+import org.bouncycastle.openpgp.PGPPublicKey;
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.equinox.internal.p2.ui.dialogs.TrustCertificateDialog;
 import org.eclipse.equinox.internal.p2.ui.dialogs.UserValidationDialog;
-import org.eclipse.equinox.internal.p2.ui.viewers.CertificateLabelProvider;
 import org.eclipse.equinox.p2.core.UIServices;
 import org.eclipse.equinox.p2.ui.LoadMetadataRepositoryJob;
 import org.eclipse.jface.dialogs.*;
-import org.eclipse.jface.viewers.*;
+import org.eclipse.jface.viewers.TreeNode;
 import org.eclipse.jface.window.Window;
 import org.eclipse.osgi.util.NLS;
 import org.eclipse.swt.SWT;
@@ -34,17 +36,19 @@
 import org.eclipse.ui.PlatformUI;
 
 /**
- * The default GUI-based implementation of {@link UIServices}.
- * The service declaration is made in the serviceui_component.xml file.
-
+ * The default GUI-based implementation of {@link UIServices}. The service
+ * declaration is made in the serviceui_component.xml file.
+ *
  */
 public class ValidationDialogServiceUI extends UIServices {
 
 	static final class MessageDialogWithLink extends MessageDialog {
 		private final String linkText;
 
-		MessageDialogWithLink(Shell parentShell, String dialogTitle, Image dialogTitleImage, String dialogMessage, int dialogImageType, String[] dialogButtonLabels, int defaultIndex, String linkText) {
-			super(parentShell, dialogTitle, dialogTitleImage, dialogMessage, dialogImageType, dialogButtonLabels, defaultIndex);
+		MessageDialogWithLink(Shell parentShell, String dialogTitle, Image dialogTitleImage, String dialogMessage,
+				int dialogImageType, String[] dialogButtonLabels, int defaultIndex, String linkText) {
+			super(parentShell, dialogTitle, dialogTitleImage, dialogMessage, dialogImageType, dialogButtonLabels,
+					defaultIndex);
 			this.linkText = linkText;
 		}
 
@@ -73,7 +77,8 @@
 	 */
 	static class OkCancelErrorDialog extends ErrorDialog {
 
-		public OkCancelErrorDialog(Shell parentShell, String dialogTitle, String message, IStatus status, int displayMask) {
+		public OkCancelErrorDialog(Shell parentShell, String dialogTitle, String message, IStatus status,
+				int displayMask) {
 			super(parentShell, dialogTitle, message, status, displayMask);
 		}
 
@@ -94,7 +99,8 @@
 			PlatformUI.getWorkbench().getDisplay().syncExec(() -> {
 				Shell shell = ProvUI.getDefaultParentShell();
 				String message = NLS.bind(ProvUIMessages.ServiceUI_LoginDetails, location);
-				UserValidationDialog dialog = new UserValidationDialog(shell, ProvUIMessages.ServiceUI_LoginRequired, null, message);
+				UserValidationDialog dialog = new UserValidationDialog(shell, ProvUIMessages.ServiceUI_LoginRequired,
+						null, message);
 				int dialogCode = dialog.open();
 				if (dialogCode == Window.OK) {
 					result[0] = dialog.getResult();
@@ -116,23 +122,37 @@
 
 	@Override
 	public TrustInfo getTrustInfo(Certificate[][] untrustedChains, final String[] unsignedDetail) {
+		return getTrustInfo(untrustedChains, Collections.emptyList(), unsignedDetail);
+	}
+
+	@Override
+	public TrustInfo getTrustInfo(Certificate[][] untrustedChains, Collection<PGPPublicKey> untrustedPublicKeys,
+			final String[] unsignedDetail) {
+		if (untrustedChains == null) {
+			untrustedChains = new Certificate[][] {};
+		}
 		boolean trustUnsigned = true;
 		boolean persistTrust = false;
-		Certificate[] trusted = new Certificate[0];
-		// Some day we may summarize all of this in one UI, or perhaps we'll have a preference to honor regarding
-		// unsigned content.  For now we prompt separately first as to whether unsigned detail should be trusted
+		List<Certificate> trustedCertificates = new ArrayList<>();
+		List<PGPPublicKey> trustedKeys = new ArrayList<>();
+		// Some day we may summarize all of this in one UI, or perhaps we'll have a
+		// preference to honor regarding
+		// unsigned content. For now we prompt separately first as to whether unsigned
+		// detail should be trusted
 		if (!isHeadless() && unsignedDetail != null && unsignedDetail.length > 0) {
-			final boolean[] result = new boolean[] {false};
+			final boolean[] result = new boolean[] { false };
 			PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() {
 				@Override
 				public void run() {
 					Shell shell = ProvUI.getDefaultParentShell();
-					OkCancelErrorDialog dialog = new OkCancelErrorDialog(shell, ProvUIMessages.ServiceUI_warning_title, null, createStatus(), IStatus.WARNING);
+					OkCancelErrorDialog dialog = new OkCancelErrorDialog(shell, ProvUIMessages.ServiceUI_warning_title,
+							null, createStatus(), IStatus.WARNING);
 					result[0] = dialog.open() == IDialogConstants.OK_ID;
 				}
 
 				private IStatus createStatus() {
-					MultiStatus parent = new MultiStatus(ProvUIActivator.PLUGIN_ID, 0, ProvUIMessages.ServiceUI_unsigned_message, null);
+					MultiStatus parent = new MultiStatus(ProvUIActivator.PLUGIN_ID, 0,
+							ProvUIMessages.ServiceUI_unsigned_message, null);
 					for (String element : unsignedDetail) {
 						parent.add(new Status(IStatus.WARNING, ProvUIActivator.PLUGIN_ID, element));
 					}
@@ -141,47 +161,57 @@
 			});
 			trustUnsigned = result[0];
 		}
-		// For now, there is no need to show certificates if there was unsigned content and we don't trust it.
+		// For now, there is no need to show certificates if there was unsigned content
+		// and we don't trust it.
 		if (!trustUnsigned)
-			return new TrustInfo(trusted, persistTrust, trustUnsigned);
+			return new TrustInfo(trustedCertificates, trustedKeys, persistTrust, trustUnsigned);
 
-		// We've established trust for unsigned content, now examine the untrusted chains
-		if (!isHeadless() && untrustedChains != null && untrustedChains.length > 0) {
-
-			final Object[] result = new Object[1];
-			final TreeNode[] input = createTreeNodes(untrustedChains);
-
+		// We've established trust for unsigned content, now examine the untrusted
+		// chains
+		if (!isHeadless() && (untrustedChains.length > 0 || !untrustedPublicKeys.isEmpty())) {
+			final TrustCertificateDialog[] dialog = new TrustCertificateDialog[1];
+			final TreeNode[] input = createTreeNodes(untrustedChains, untrustedPublicKeys);
 			PlatformUI.getWorkbench().getDisplay().syncExec(() -> {
 				Shell shell = ProvUI.getDefaultParentShell();
-				ILabelProvider labelProvider = new CertificateLabelProvider();
-				TreeNodeContentProvider contentProvider = new TreeNodeContentProvider();
-				TrustCertificateDialog trustCertificateDialog = new TrustCertificateDialog(shell, input, labelProvider, contentProvider);
+				TrustCertificateDialog trustCertificateDialog = new TrustCertificateDialog(shell, input);
+				dialog[0] = trustCertificateDialog;
 				trustCertificateDialog.open();
-				Certificate[] values = new Certificate[trustCertificateDialog.getResult() == null ? 0 : trustCertificateDialog.getResult().length];
-				for (int i = 0; i < values.length; i++) {
-					values[i] = (Certificate) ((TreeNode) trustCertificateDialog.getResult()[i]).getValue();
-				}
-				result[0] = values;
 			});
 			persistTrust = true;
-			trusted = (Certificate[]) result[0];
+			if (dialog[0].getResult() != null) {
+				for (Object o : dialog[0].getResult()) {
+					if (o instanceof TreeNode) {
+						o = ((TreeNode) o).getValue();
+					}
+					if (o instanceof Certificate) {
+						trustedCertificates.add((Certificate) o);
+					} else if (o instanceof PGPPublicKey) {
+						trustedKeys.add((PGPPublicKey) o);
+					}
+				}
+			}
 		}
-		return new TrustInfo(trusted, persistTrust, trustUnsigned);
+		return new TrustInfo(trustedCertificates, trustedKeys, persistTrust, trustUnsigned);
 	}
 
-	private TreeNode[] createTreeNodes(Certificate[][] certificates) {
-		TreeNode[] children = new TreeNode[certificates.length];
-		for (int i = 0; i < certificates.length; i++) {
+	private TreeNode[] createTreeNodes(Certificate[][] certificates, Collection<PGPPublicKey> untrustedPublicKeys) {
+		TreeNode[] children = new TreeNode[certificates.length + untrustedPublicKeys.size()];
+		int i = 0;
+		for (i = 0; i < certificates.length; i++) {
 			TreeNode head = new TreeNode(certificates[i][0]);
 			TreeNode parent = head;
 			children[i] = head;
 			for (int j = 0; j < certificates[i].length; j++) {
 				TreeNode node = new TreeNode(certificates[i][j]);
 				node.setParent(parent);
-				parent.setChildren(new TreeNode[] {node});
+				parent.setChildren(new TreeNode[] { node });
 				parent = node;
 			}
 		}
+		for (PGPPublicKey key : untrustedPublicKeys) {
+			children[i] = new TreeNode(key);
+			i++;
+		}
 		return children;
 	}
 
@@ -197,7 +227,8 @@
 				else
 					message = NLS.bind(ProvUIMessages.ProvUIMessages_NotAccepted_EnterFor_0, location);
 
-				UserValidationDialog dialog = new UserValidationDialog(previousInfo, shell, ProvUIMessages.ServiceUI_LoginRequired, null, message);
+				UserValidationDialog dialog = new UserValidationDialog(previousInfo, shell,
+						ProvUIMessages.ServiceUI_LoginRequired, null, message);
 				int dialogCode = dialog.open();
 				if (dialogCode == Window.OK) {
 					result[0] = dialog.getResult();
@@ -216,14 +247,15 @@
 			return;
 		}
 		PlatformUI.getWorkbench().getDisplay().syncExec(() -> {
-			MessageDialog dialog = new MessageDialogWithLink(ProvUI.getDefaultParentShell(), title, null, text, MessageDialog.INFORMATION, new String[] {IDialogConstants.OK_LABEL}, 0, linkText);
+			MessageDialog dialog = new MessageDialogWithLink(ProvUI.getDefaultParentShell(), title, null, text,
+					MessageDialog.INFORMATION, new String[] { IDialogConstants.OK_LABEL }, 0, linkText);
 			dialog.open();
 		});
 	}
 
 	private boolean isHeadless() {
 		// If there is no UI available and we are still the IServiceUI,
-		// assume that the operation should proceed.  See
+		// assume that the operation should proceed. See
 		// https://bugs.eclipse.org/bugs/show_bug.cgi?id=291049
 		return !PlatformUI.isWorkbenchRunning();
 	}
diff --git a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/dialogs/TrustCertificateDialog.java b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/dialogs/TrustCertificateDialog.java
index 4605922..a37911a 100644
--- a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/dialogs/TrustCertificateDialog.java
+++ b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/dialogs/TrustCertificateDialog.java
@@ -21,11 +21,15 @@
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.function.Function;
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.openpgp.PGPPublicKey;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.equinox.internal.p2.ui.ProvUIActivator;
 import org.eclipse.equinox.internal.p2.ui.ProvUIMessages;
 import org.eclipse.equinox.internal.p2.ui.viewers.CertificateLabelProvider;
+import org.eclipse.equinox.internal.provisional.security.ui.X500PrincipalHelper;
 import org.eclipse.equinox.internal.provisional.security.ui.X509CertificateViewDialog;
 import org.eclipse.jface.dialogs.Dialog;
 import org.eclipse.jface.dialogs.IDialogConstants;
@@ -45,29 +49,51 @@
 
 	private Object inputElement;
 	private IStructuredContentProvider contentProvider;
-	private ILabelProvider labelProvider;
 
-	private final static int SIZING_SELECTION_WIDGET_HEIGHT = 250;
-	private final static int SIZING_SELECTION_WIDGET_WIDTH = 300;
+	private static final int SIZING_SELECTION_WIDGET_HEIGHT = 250;
+	private static final int SIZING_SELECTION_WIDGET_WIDTH = 300;
 
 	CheckboxTableViewer listViewer;
 
 	private TreeViewer certificateChainViewer;
-	private Button detailsButton;
 	protected TreeNode parentElement;
 	protected Object selectedCertificate;
+	private Button detailsButton;
 
-	public TrustCertificateDialog(Shell parentShell, Object input, ILabelProvider labelProvider,
-			ITreeContentProvider contentProvider) {
+	public TrustCertificateDialog(Shell parentShell, Object input) {
 		super(parentShell);
 		inputElement = input;
-		this.contentProvider = contentProvider;
-		this.labelProvider = labelProvider;
+		this.contentProvider = new TreeNodeContentProvider();
 		setTitle(ProvUIMessages.TrustCertificateDialog_Title);
 		setMessage(ProvUIMessages.TrustCertificateDialog_Message);
 		setShellStyle(SWT.DIALOG_TRIM | SWT.MODELESS | SWT.RESIZE | getDefaultOrientation());
 	}
 
+	private static class PGPOrX509ColumnLabelProvider extends ColumnLabelProvider {
+		private Function<PGPPublicKey, String> pgpMap;
+		private Function<X509Certificate, String> x509map;
+
+		public PGPOrX509ColumnLabelProvider(Function<PGPPublicKey, String> pgpMap,
+				Function<X509Certificate, String> x509map) {
+			this.pgpMap = pgpMap;
+			this.x509map = x509map;
+		}
+
+		@Override
+		public String getText(Object element) {
+			if (element instanceof TreeNode) {
+				element = ((TreeNode) element).getValue();
+			}
+			if (element instanceof PGPPublicKey) {
+				return pgpMap.apply((PGPPublicKey) element);
+			}
+			if (element instanceof X509Certificate) {
+				return x509map.apply((X509Certificate) element);
+			}
+			return super.getText(element);
+		}
+	}
+
 	@Override
 	protected Control createDialogArea(Composite parent) {
 		Composite composite = createUpperDialogArea(parent);
@@ -94,6 +120,7 @@
 		listViewer.addDoubleClickListener(getDoubleClickListener());
 		listViewer.addSelectionChangedListener(getParentSelectionListener());
 		createButtons(composite);
+		detailsButton.setEnabled(selectedCertificate instanceof X509Certificate);
 		return composite;
 	}
 
@@ -140,11 +167,12 @@
 				if (selectedCertificate instanceof TreeNode) {
 					o = ((TreeNode) selectedCertificate).getValue();
 				}
+				FileDialog destination = new FileDialog(detailsButton.getShell(), SWT.SAVE);
+				destination.setText(ProvUIMessages.TrustCertificateDialog_Export);
 				if (o instanceof X509Certificate) {
 					X509Certificate cert = (X509Certificate) o;
-					FileDialog destination = new FileDialog(detailsButton.getShell(), SWT.SAVE);
 					destination.setFilterExtensions(new String[] { "*.der" }); //$NON-NLS-1$
-					destination.setText(ProvUIMessages.TrustCertificateDialog_Export);
+					destination.setFileName(cert.getSerialNumber().toString() + ".der"); //$NON-NLS-1$
 					String path = destination.open();
 					if (path == null) {
 						return;
@@ -156,6 +184,21 @@
 						ProvUIActivator.getDefault().getLog()
 								.log(new Status(IStatus.ERROR, ProvUIActivator.PLUGIN_ID, ex.getMessage(), ex));
 					}
+				} else if (o instanceof PGPPublicKey) {
+					PGPPublicKey key = (PGPPublicKey) o;
+					destination.setFilterExtensions(new String[] { "*.asc" }); //$NON-NLS-1$
+					destination.setFileName(key.getKeyID() + ".asc"); //$NON-NLS-1$
+					String path = destination.open();
+					if (path == null) {
+						return;
+					}
+					File destinationFile = new File(path);
+					try (OutputStream output = new ArmoredOutputStream(new FileOutputStream(destinationFile))) {
+						output.write(key.getEncoded());
+					} catch (IOException ex) {
+						ProvUIActivator.getDefault().getLog()
+								.log(new Status(IStatus.ERROR, ProvUIActivator.PLUGIN_ID, ex.getMessage(), ex));
+					}
 				}
 			}
 
@@ -163,6 +206,7 @@
 			public void widgetSelected(SelectionEvent e) {
 				widgetDefaultSelected(e);
 			}
+
 		});
 	}
 
@@ -177,8 +221,29 @@
 		data.widthHint = SIZING_SELECTION_WIDGET_WIDTH;
 		listViewer.getTable().setLayoutData(data);
 
-		listViewer.setLabelProvider(labelProvider);
 		listViewer.setContentProvider(contentProvider);
+		TableViewerColumn typeColumn = new TableViewerColumn(listViewer, SWT.NONE);
+		typeColumn.getColumn().setWidth(80);
+		typeColumn.getColumn().setText(ProvUIMessages.TrustCertificateDialog_ObjectType);
+		typeColumn.setLabelProvider(new PGPOrX509ColumnLabelProvider(key -> "PGP", cert -> "x509")); //$NON-NLS-1$ //$NON-NLS-2$
+		TableViewerColumn idColumn = new TableViewerColumn(listViewer, SWT.NONE);
+		idColumn.getColumn().setWidth(200);
+		idColumn.getColumn().setText(ProvUIMessages.TrustCertificateDialog_Id);
+		idColumn.setLabelProvider(new PGPOrX509ColumnLabelProvider(key -> Long.toString(key.getKeyID()),
+				cert -> cert.getSerialNumber().toString()));
+		TableViewerColumn signerColumn = new TableViewerColumn(listViewer, SWT.NONE);
+		signerColumn.getColumn().setText(ProvUIMessages.TrustCertificateDialog_Name);
+		signerColumn.getColumn().setWidth(400);
+		signerColumn.setLabelProvider(new PGPOrX509ColumnLabelProvider(pgp -> {
+			java.util.List<String> users = new ArrayList<>();
+			pgp.getUserIDs().forEachRemaining(users::add);
+			return String.join(",", users); //$NON-NLS-1$
+		}, x509 -> {
+			X500PrincipalHelper principalHelper = new X500PrincipalHelper(x509.getSubjectX500Principal());
+			return principalHelper.getCN() + "; " + principalHelper.getOU() + "; " //$NON-NLS-1$ //$NON-NLS-2$
+					+ principalHelper.getO();
+		}));
+		listViewer.getTable().setHeaderVisible(true);
 
 		addSelectionButtons(composite);
 
@@ -242,6 +307,7 @@
 			ISelection selection = event.getSelection();
 			if (selection instanceof StructuredSelection) {
 				selectedCertificate = ((StructuredSelection) selection).getFirstElement();
+				detailsButton.setEnabled(selectedCertificate instanceof X509Certificate);
 			}
 		};
 	}
diff --git a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/messages.properties b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/messages.properties
index 8dd78ac..561551a 100644
--- a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/messages.properties
+++ b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/messages.properties
@@ -278,8 +278,11 @@
 
 TrustCertificateDialog_Details=\uD83D\uDD0D &Details...
 TrustCertificateDialog_Export=\uD83D\uDCE5 E&xport...
-TrustCertificateDialog_Title=Certificates
-TrustCertificateDialog_Message=Do you trust these certificates?
-TrustCertificateDialog_AcceptSelectedButtonLabel=&Accept selected
+TrustCertificateDialog_Title=Trust
+TrustCertificateDialog_Message=Do you trust these signers?
+TrustCertificateDialog_AcceptSelectedButtonLabel=&Trust selected
 TrustCertificateDialog_SelectAll=&Select All
 TrustCertificateDialog_DeselectAll=&Deselect All
+TrustCertificateDialog_ObjectType=Type
+TrustCertificateDialog_Id=Id
+TrustCertificateDialog_Name=Name
diff --git a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/viewers/CertificateLabelProvider.java b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/viewers/CertificateLabelProvider.java
index 16246ed..9eb67d0 100644
--- a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/viewers/CertificateLabelProvider.java
+++ b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/viewers/CertificateLabelProvider.java
@@ -13,8 +13,8 @@
  *******************************************************************************/
 package org.eclipse.equinox.internal.p2.ui.viewers;
 
-import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
+import org.bouncycastle.openpgp.PGPPublicKey;
 import org.eclipse.equinox.internal.provisional.security.ui.X500PrincipalHelper;
 import org.eclipse.jface.viewers.*;
 import org.eclipse.swt.graphics.Image;
@@ -31,17 +31,37 @@
 
 	@Override
 	public String getText(Object element) {
-		Certificate cert = null;
 		if (element instanceof TreeNode) {
-			cert = (Certificate) ((TreeNode) element).getValue();
-		}
-		if (cert != null) {
-			X500PrincipalHelper principalHelper = new X500PrincipalHelper(((X509Certificate) cert).getSubjectX500Principal());
-			return principalHelper.getCN() + "; " + principalHelper.getOU() + "; " + principalHelper.getO(); //$NON-NLS-1$ //$NON-NLS-2$
+			Object o = ((TreeNode) element).getValue();
+			if (o instanceof X509Certificate) {
+				X509Certificate cert = (X509Certificate) o;
+				X500PrincipalHelper principalHelper = new X500PrincipalHelper(cert.getSubjectX500Principal());
+				return principalHelper.getCN() + "; " + principalHelper.getOU() + "; " //$NON-NLS-1$ //$NON-NLS-2$
+						+ principalHelper.getO();
+			} else if (o instanceof PGPPublicKey) {
+				return userFriendlyFingerPrint((PGPPublicKey) o);
+			}
 		}
 		return ""; //$NON-NLS-1$
 	}
 
+	private String userFriendlyFingerPrint(PGPPublicKey key) {
+		if (key == null) {
+			return null;
+		}
+		StringBuilder builder = new StringBuilder();
+		boolean spaceSuffix = false;
+		for (byte b : key.getFingerprint()) {
+			builder.append(String.format("%02X", Byte.toUnsignedInt(b))); //$NON-NLS-1$
+			if (spaceSuffix) {
+				builder.append(' ');
+			}
+			spaceSuffix = !spaceSuffix;
+		}
+		builder.deleteCharAt(builder.length() - 1);
+		return builder.toString();
+	}
+
 	@Override
 	public void addListener(ILabelProviderListener listener) {
 		// do nothing