Bug 578166 - Signing information apparent in the Plug-ins tab of the
About dialog

Introduce org.eclipse.equinox.internal.p2.ui.KeySigningInfoFactory to
provide the adapter for the AboutBundleData.ExtendedSigningInfo
interface.  The signature and key information is gathered from the
artifact metadata of the bundle pool repository of the installation.

Change-Id: I578b62cb0a20d4141d38c2b587e39ae8bc0dfd4d
Signed-off-by: Ed Merks <ed.merks@gmail.com>
Reviewed-on: https://git.eclipse.org/r/c/equinox/rt.equinox.p2/+/190744
diff --git a/bundles/org.eclipse.equinox.p2.artifact.repository/src/org/eclipse/equinox/internal/p2/artifact/processors/pgp/PGPPublicKeyStore.java b/bundles/org.eclipse.equinox.p2.artifact.repository/src/org/eclipse/equinox/internal/p2/artifact/processors/pgp/PGPPublicKeyStore.java
index c5f9d01..383fd74 100644
--- a/bundles/org.eclipse.equinox.p2.artifact.repository/src/org/eclipse/equinox/internal/p2/artifact/processors/pgp/PGPPublicKeyStore.java
+++ b/bundles/org.eclipse.equinox.p2.artifact.repository/src/org/eclipse/equinox/internal/p2/artifact/processors/pgp/PGPPublicKeyStore.java
@@ -25,7 +25,7 @@
 import org.eclipse.equinox.p2.repository.spi.PGPPublicKeyService;
 
 public class PGPPublicKeyStore {
-	private Map<String, PGPPublicKey> keys = new HashMap<>();
+	private Map<String, PGPPublicKey> keys = new LinkedHashMap<>();
 
 	public PGPPublicKey addKey(PGPPublicKey key) {
 		if (key == null) {
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 6fc1dc5..740e06c 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
@@ -88,6 +88,13 @@
 		return res;
 	}
 
+	public static PGPPublicKeyStore getKeys(IArtifactDescriptor artifact) {
+		PGPPublicKeyStore keyStore = new PGPPublicKeyStore();
+		String keyText = artifact.getProperty(PGPSignatureVerifier.PGP_SIGNER_KEYS_PROPERTY_NAME);
+		PGPPublicKeyStore.readPublicKeys(keyText).stream().forEach(keyStore::addKey);
+		return keyStore;
+	}
+
 	@Override
 	public void initialize(IProvisioningAgent agent, IProcessingStepDescriptor descriptor,
 			IArtifactDescriptor context) {
diff --git a/bundles/org.eclipse.equinox.p2.extensionlocation/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.p2.extensionlocation/META-INF/MANIFEST.MF
index 76a2520..290732d 100644
--- a/bundles/org.eclipse.equinox.p2.extensionlocation/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.p2.extensionlocation/META-INF/MANIFEST.MF
@@ -2,11 +2,11 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.equinox.p2.extensionlocation;singleton:=true
-Bundle-Version: 1.4.0.qualifier
+Bundle-Version: 1.4.100.qualifier
 Bundle-Activator: org.eclipse.equinox.internal.p2.extensionlocation.Activator
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
-Export-Package: org.eclipse.equinox.internal.p2.extensionlocation;x-friends:="org.eclipse.equinox.p2.reconciler.dropins,org.eclipse.equinox.p2.ui.importexport"
+Export-Package: org.eclipse.equinox.internal.p2.extensionlocation;x-friends:="org.eclipse.equinox.p2.reconciler.dropins,org.eclipse.equinox.p2.ui,org.eclipse.equinox.p2.ui.importexport"
 Require-Bundle: org.eclipse.equinox.common;bundle-version="[3.5.0,4.0.0)",
  org.eclipse.equinox.p2.metadata
 Bundle-RequiredExecutionEnvironment: JavaSE-11
diff --git a/bundles/org.eclipse.equinox.p2.extensionlocation/pom.xml b/bundles/org.eclipse.equinox.p2.extensionlocation/pom.xml
index 474ce10..d635219 100644
--- a/bundles/org.eclipse.equinox.p2.extensionlocation/pom.xml
+++ b/bundles/org.eclipse.equinox.p2.extensionlocation/pom.xml
@@ -9,6 +9,6 @@
   </parent>
   <groupId>org.eclipse.equinox</groupId>
   <artifactId>org.eclipse.equinox.p2.extensionlocation</artifactId>
-  <version>1.4.0-SNAPSHOT</version>
+  <version>1.4.100-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 </project>
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 f4c5972..b66eb2c 100644
--- a/bundles/org.eclipse.equinox.p2.ui/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.p2.ui/META-INF/MANIFEST.MF
@@ -43,6 +43,7 @@
  org.eclipse.equinox.internal.p2.artifact.repository,
  org.eclipse.equinox.internal.p2.core.helpers,
  org.eclipse.equinox.internal.p2.director,
+ org.eclipse.equinox.internal.p2.extensionlocation,
  org.eclipse.equinox.internal.p2.metadata,
  org.eclipse.equinox.internal.p2.metadata.repository,
  org.eclipse.equinox.internal.p2.operations,
diff --git a/bundles/org.eclipse.equinox.p2.ui/plugin.xml b/bundles/org.eclipse.equinox.p2.ui/plugin.xml
index 6f6031e..87e8645 100644
--- a/bundles/org.eclipse.equinox.p2.ui/plugin.xml
+++ b/bundles/org.eclipse.equinox.p2.ui/plugin.xml
@@ -12,6 +12,13 @@
          <adapter type="org.eclipse.equinox.p2.repository.metadata.IMetadataRepository"/>
          <adapter type="org.eclipse.equinox.p2.repository.artifact.IArtifactRepository"/>
       </factory>
+      <factory
+            adaptableType="org.eclipse.ui.internal.about.AboutPluginsPage"
+            class="org.eclipse.equinox.internal.p2.ui.KeySigningInfoFactory">
+         <adapter
+               type="org.eclipse.ui.internal.about.AboutBundleData$ExtendedSigningInfo">
+         </adapter>
+      </factory>
    </extension>
    
      <extension
diff --git a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/KeySigningInfoFactory.java b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/KeySigningInfoFactory.java
new file mode 100644
index 0000000..782dff9
--- /dev/null
+++ b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/KeySigningInfoFactory.java
@@ -0,0 +1,184 @@
+/*******************************************************************************
+ * Copyright (c) 2022 Eclipse contributors and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *******************************************************************************/
+package org.eclipse.equinox.internal.p2.ui;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+import org.bouncycastle.openpgp.*;
+import org.eclipse.core.runtime.*;
+import org.eclipse.equinox.internal.p2.artifact.processors.pgp.PGPPublicKeyStore;
+import org.eclipse.equinox.internal.p2.artifact.processors.pgp.PGPSignatureVerifier;
+import org.eclipse.equinox.p2.core.IProvisioningAgent;
+import org.eclipse.equinox.p2.metadata.IArtifactKey;
+import org.eclipse.equinox.p2.query.IQueryResult;
+import org.eclipse.equinox.p2.repository.artifact.*;
+import org.eclipse.equinox.p2.repository.spi.PGPPublicKeyService;
+import org.eclipse.ui.internal.about.AboutBundleData;
+import org.eclipse.ui.internal.about.AboutPluginsPage;
+import org.osgi.framework.Bundle;
+
+/**
+ * A factory used by the {@link AboutPluginsPage} to provide extended signing
+ * information, in particular information about PGP signing.
+ *
+ * @since 2.7.400
+ */
+public class KeySigningInfoFactory implements IAdapterFactory {
+
+	private static final Class<?>[] CLASSES = new Class[] { AboutBundleData.ExtendedSigningInfo.class };
+
+	@Override
+	public <T> T getAdapter(Object adaptableObject, Class<T> adapterType) {
+		if (adapterType == AboutBundleData.ExtendedSigningInfo.class) {
+			return adapterType.cast(new AboutBundleData.ExtendedSigningInfo() {
+				private final Map<File, Map<PGPSignature, PGPPublicKey>> bundlePoolArtficactSigningDetails = getBundlePoolArtficactPGPSigningDetails();
+
+				@Override
+				public boolean isSigned(Bundle bundle) {
+					return getDetails(bundle) != null;
+				}
+
+				@Override
+				public String getSigningType(Bundle bundle) {
+					return ProvUIMessages.KeySigningInfoFactory_PGPSigningType;
+				}
+
+				@Override
+				public String getSigningDetails(Bundle bundle) {
+					Map<PGPSignature, PGPPublicKey> details = getDetails(bundle);
+					if (details != null) {
+						PGPPublicKeyService keyService = getKeyService();
+						List<String> lines = new ArrayList<>();
+						for (PGPPublicKey key : details.values()) {
+							if (keyService != null) {
+								// Be sure to normalize/enhance the key so we properly don't show
+								// self-signatures.
+								key = keyService.addKey(key);
+							}
+							if (!lines.isEmpty()) {
+								lines.add(""); //$NON-NLS-1$
+							}
+							addDetails(key, lines, ""); //$NON-NLS-1$
+							if (keyService != null) {
+								Set<PGPPublicKey> verifiedCertifications = keyService.getVerifiedCertifications(key);
+								boolean first = true;
+								for (PGPPublicKey verifyingKey : verifiedCertifications) {
+									/// Don't show self-signatures.
+									if (!verifyingKey.equals(key)) {
+										if (first) {
+											lines.add("  " + ProvUIMessages.KeySigningInfoFactory_KeySignersSection); //$NON-NLS-1$
+											first = false;
+										}
+										addDetails(verifyingKey, lines, "    "); //$NON-NLS-1$
+									}
+								}
+							}
+						}
+						return String.join("\n", lines); //$NON-NLS-1$
+					}
+					return null;
+				}
+
+				@Override
+				public Date getSigningTime(Bundle bundle) {
+					Map<PGPSignature, PGPPublicKey> details = getDetails(bundle);
+					return details == null ? null : details.keySet().iterator().next().getCreationTime();
+				}
+
+				private void addDetails(PGPPublicKey key, List<String> lines, String indentation) {
+					lines.add(indentation + ProvUIMessages.KeySigningInfoFactory_FingerprintItem
+							+ PGPPublicKeyService.toHex(key.getFingerprint()).toUpperCase(Locale.ROOT));
+					for (Iterator<String> userIDs = key.getUserIDs(); userIDs.hasNext();) {
+						lines.add(indentation + ProvUIMessages.KeySigningInfoFactory_UserIDItem + userIDs.next());
+					}
+				}
+
+				private PGPPublicKeyService getKeyService() {
+					IProvisioningAgent agent = org.eclipse.equinox.internal.p2.extensionlocation.Activator
+							.getCurrentAgent();
+					return agent == null ? null : agent.getService(PGPPublicKeyService.class);
+				}
+
+				private Map<PGPSignature, PGPPublicKey> getDetails(Bundle bundle) {
+					try {
+						File bundleFile = FileLocator.getBundleFile(bundle);
+						Map<PGPSignature, PGPPublicKey> details = bundlePoolArtficactSigningDetails.get(bundleFile);
+						return details;
+					} catch (IOException e) {
+						ProvUIActivator.getDefault().getLog()
+								.log(new Status(IStatus.ERROR, ProvUIActivator.PLUGIN_ID, e.getMessage(), e));
+						return null;
+					}
+				}
+			});
+		}
+
+		return null;
+	}
+
+	@Override
+	public Class<?>[] getAdapterList() {
+		return CLASSES;
+	}
+
+	/**
+	 * Returns a map from artifact files to the PGP signature/key pairs that were
+	 * used to {@link PGPSignatureVerifier#close() verify} the artifact while it was
+	 * being downloaded.
+	 *
+	 * @return a map of all the PGP signed artifact files to their signature/key
+	 *         pairs.
+	 */
+	private static Map<File, Map<PGPSignature, PGPPublicKey>> getBundlePoolArtficactPGPSigningDetails() {
+		Map<File, Map<PGPSignature, PGPPublicKey>> result = new LinkedHashMap<>();
+		try {
+			// Look up artifact metadata for all the bundle pool repository of the
+			// installation.
+			IFileArtifactRepository bundlePoolRepository = org.eclipse.equinox.internal.p2.extensionlocation.Activator
+					.getBundlePoolRepository();
+			IQueryResult<IArtifactKey> allArtifactKeys = bundlePoolRepository.query(ArtifactKeyQuery.ALL_KEYS,
+					new NullProgressMonitor());
+			for (IArtifactKey key : allArtifactKeys) {
+				for (IArtifactDescriptor descriptor : bundlePoolRepository.getArtifactDescriptors(key)) {
+					File file = bundlePoolRepository.getArtifactFile(descriptor);
+					if (file != null) {
+						try {
+							Collection<PGPSignature> signatures = PGPSignatureVerifier.getSignatures(descriptor);
+							if (!signatures.isEmpty()) {
+								Map<PGPSignature, PGPPublicKey> details = new LinkedHashMap<>();
+								PGPPublicKeyStore keys = PGPSignatureVerifier.getKeys(descriptor);
+								for (PGPSignature signature : signatures) {
+									Collection<PGPPublicKey> signingKeys = keys.getKeys(signature.getKeyID());
+									if (!signingKeys.isEmpty()) {
+										// There is vanishingly small chance that two keys with colliding key IDs were
+										// used on two different signatures on the same artifact.
+										details.put(signature, signingKeys.iterator().next());
+									}
+								}
+								if (!details.isEmpty()) {
+									result.put(file, details);
+								}
+							}
+						} catch (IOException | PGPException | RuntimeException e) {
+							ProvUIActivator.getDefault().getLog()
+									.log(new Status(IStatus.ERROR, ProvUIActivator.PLUGIN_ID, e.getMessage(), e));
+						}
+					}
+				}
+			}
+		} catch (RuntimeException e) {
+			ProvUIActivator.getDefault().getLog()
+					.log(new Status(IStatus.ERROR, ProvUIActivator.PLUGIN_ID, e.getMessage(), e));
+		}
+		return result;
+	}
+}
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 af37549..e3804b9 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
@@ -247,6 +247,10 @@
 	public static String SelectableIUsPage_Deselect_All;
 	public static String InstallRemediationPage_Title;
 	public static String InstallRemediationPage_Description;
+	public static String KeySigningInfoFactory_FingerprintItem;
+	public static String KeySigningInfoFactory_KeySignersSection;
+	public static String KeySigningInfoFactory_PGPSigningType;
+	public static String KeySigningInfoFactory_UserIDItem;
 	public static String UpdateRemediationPage_Title;
 	public static String UpdateRemediationPage_Description;
 	public static String RemediationPage_SubDescription;
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 c9fdafb..26fc363 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
@@ -257,6 +257,10 @@
 RepositoryTracker_DuplicateLocation=Duplicate location
 MetadataRepositoryElement_RepositoryLoadError=Error loading repository {0}
 IUViewQueryContext_NoCategorizedItemsDescription=You can uncheck the 'Group items by category' check box to see items without categories.
+KeySigningInfoFactory_FingerprintItem=Fingerprint=
+KeySigningInfoFactory_KeySignersSection=Key Signers:
+KeySigningInfoFactory_PGPSigningType=PGP Public Key
+KeySigningInfoFactory_UserIDItem=UserID=
 QueriedElementWrapper_NoCategorizedItemsExplanation=There are no categorized items
 QueriedElementWrapper_NoItemsExplanation=There are no items available
 QueriedElementWrapper_SiteNotFound=Could not find {0}