Bug 578232 - Improve trust preference page
Provide support for showing details of PGP keys in a format similar to
what key servers display.
Change-Id: I17c53ce9252c61c1c4592d0ae4fbdf585dd98c9f
Signed-off-by: Ed Merks <ed.merks@gmail.com>
Reviewed-on: https://git.eclipse.org/r/c/equinox/rt.equinox.p2/+/190746
diff --git a/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/ProvSDKMessages.java b/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/ProvSDKMessages.java
index 3470f3e..26d9793 100644
--- a/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/ProvSDKMessages.java
+++ b/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/ProvSDKMessages.java
@@ -64,6 +64,7 @@
public static String TrustPreferencePage_DateNotYetvalid;
public static String TrustPreferencePage_DateNotYetValid;
public static String TrustPreferencePage_DateValid;
+ public static String TrustPreferencePage_Details;
public static String TrustPreferencePage_Export;
public static String TrustPreferencePage_FingerprintIdColumn;
public static String TrustPreferencePage_NameColumn;
diff --git a/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/TrustPreferencePage.java b/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/TrustPreferencePage.java
index 957f190..1a8f1de 100644
--- a/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/TrustPreferencePage.java
+++ b/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/TrustPreferencePage.java
@@ -26,6 +26,7 @@
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.artifact.processors.pgp.PGPPublicKeyStore;
import org.eclipse.equinox.internal.p2.engine.phases.CertificateChecker;
+import org.eclipse.equinox.internal.p2.ui.dialogs.PGPPublicKeyViewDialog;
import org.eclipse.equinox.internal.p2.ui.viewers.CertificateLabelProvider;
import org.eclipse.equinox.p2.core.IProvisioningAgent;
import org.eclipse.equinox.p2.engine.IProfileRegistry;
@@ -143,7 +144,8 @@
return ProvSDKMessages.TrustPreferencePage_DateValid;
}
Instant expires = pgp.getCreationTime().toInstant().plus(validSeconds, ChronoUnit.SECONDS);
- return expires.isBefore(Instant.now()) ? NLS.bind(ProvSDKMessages.TrustPreferencePage_DateExpiredSince, expires)
+ return expires.isBefore(Instant.now())
+ ? NLS.bind(ProvSDKMessages.TrustPreferencePage_DateExpiredSince, expires)
: NLS.bind(ProvSDKMessages.TrustPreferencePage_DataValidExpires, expires);
}, x509 -> {
try {
@@ -265,12 +267,30 @@
removeButton.setEnabled(false);
setVerticalButtonLayoutData(removeButton);
+ Runnable details = () -> {
+ Object element = viewer.getStructuredSelection().getFirstElement();
+ if (element instanceof X509Certificate) {
+ // create and open dialog for certificate chain
+ CertificateLabelProvider.openDialog(getShell(), (X509Certificate) element);
+ } else {
+ new PGPPublicKeyViewDialog(getShell(), (PGPPublicKey) element,
+ provisioningAgent.getService(PGPPublicKeyService.class)).open();
+ }
+ };
+
+ Button detailsButton = new Button(buttonComposite, SWT.PUSH);
+ detailsButton.setText(ProvSDKMessages.TrustPreferencePage_Details);
+ detailsButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> details.run()));
+ detailsButton.setEnabled(false);
+ setVerticalButtonLayoutData(detailsButton);
+
viewer.addPostSelectionChangedListener(e -> {
List<Object> selectedKeys = getSelectedKeys();
exportButton.setEnabled(selectedKeys.size() == 1);
Collection<PGPPublicKey> keys = trustedKeys.all();
removeButton.setEnabled(
selectedKeys.stream().anyMatch(o -> keys.contains(o) || trustedCertificates.contains(o)));
+ detailsButton.setEnabled(selectedKeys.size() == 1);
});
Button trustAllButton = WidgetFactory.button(SWT.CHECK).text(ProvSDKMessages.TrustPreferencePage_TrustAll)
@@ -306,14 +326,7 @@
}
}));
- viewer.addDoubleClickListener(e -> {
- StructuredSelection selection = (StructuredSelection) e.getSelection();
- Object element = selection.getFirstElement();
- if (element instanceof X509Certificate) {
- // create and open dialog for certificate chain
- CertificateLabelProvider.openDialog(getShell(), (X509Certificate) element);
- }
- });
+ viewer.addDoubleClickListener(e -> details.run());
typeColumn.getColumn().pack();
diff --git a/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/messages.properties b/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/messages.properties
index 3b1c616..93df84f 100644
--- a/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/messages.properties
+++ b/bundles/org.eclipse.equinox.p2.ui.sdk/src/org/eclipse/equinox/internal/p2/ui/sdk/messages.properties
@@ -48,6 +48,7 @@
TrustPreferencePage_DateNotYetvalid=\u274C Not yet valid
TrustPreferencePage_DateNotYetValid=\u274C Not yet valid, starts {0}
TrustPreferencePage_DateValid=\u2714\uFE0F Valid
+TrustPreferencePage_Details=\uD83D\uDD0D D&etails...
TrustPreferencePage_Export=Export...
TrustPreferencePage_FingerprintIdColumn=Fingerprint/Id
TrustPreferencePage_NameColumn=Name
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 b66eb2c..433d2e8 100644
--- a/bundles/org.eclipse.equinox.p2.ui/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.p2.ui/META-INF/MANIFEST.MF
@@ -19,6 +19,7 @@
org.eclipse.equinox.internal.p2.ui.dialogs;
x-friends:="org.eclipse.equinox.p2.ui.admin,
org.eclipse.equinox.p2.ui.sdk.scheduler,
+ org.eclipse.equinox.p2.ui.sdk,
org.eclipse.pde.ui,
org.eclipse.equinox.p2.ui.importexport",
org.eclipse.equinox.internal.p2.ui.model;
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 e3804b9..f79aacc 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
@@ -78,6 +78,7 @@
public static String IUGeneralInfoPropertyPage_VersionLabel;
public static String IULicensePropertyPage_NoLicense;
public static String IULicensePropertyPage_ViewLicenseLabel;
+ public static String PGPPublicKeyViewDialog_Title;
public static String ProfileModificationAction_InvalidSelections;
public static String ProfileModificationWizardPage_DetailsLabel;
public static String ProfileSnapshots_Label;
diff --git a/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/dialogs/PGPPublicKeyViewDialog.java b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/dialogs/PGPPublicKeyViewDialog.java
new file mode 100644
index 0000000..e02936a
--- /dev/null
+++ b/bundles/org.eclipse.equinox.p2.ui/src/org/eclipse/equinox/internal/p2/ui/dialogs/PGPPublicKeyViewDialog.java
@@ -0,0 +1,248 @@
+/*******************************************************************************
+ * 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.dialogs;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.List;
+import org.bouncycastle.bcpg.*;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.equinox.internal.p2.ui.ProvUIMessages;
+import org.eclipse.equinox.p2.repository.spi.PGPPublicKeyService;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.viewers.StyledString;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.graphics.*;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.*;
+
+/**
+ * Presents information about a key in a format similar to what key servers
+ * display.
+ *
+ * @since 1.2.4
+ */
+public class PGPPublicKeyViewDialog extends TitleAreaDialog {
+
+ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); //$NON-NLS-1$
+
+ final private PGPPublicKey originalKey;
+
+ final private PGPPublicKeyService keyService;
+
+ private StyledText styledText;
+
+ public PGPPublicKeyViewDialog(Shell parentShell, PGPPublicKey key, PGPPublicKeyService keyService) {
+ super(parentShell);
+ this.originalKey = key;
+ this.keyService = keyService;
+ DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); //$NON-NLS-1$
+ }
+
+ @Override
+ protected void configureShell(Shell newShell) {
+ super.configureShell(newShell);
+ newShell.setText(ProvUIMessages.PGPPublicKeyViewDialog_Title);
+ if (keyService != null) {
+ computeVerifiedCertifications(newShell);
+ }
+ }
+
+ @Override
+ protected void setShellStyle(int newShellStyle) {
+ super.setShellStyle(newShellStyle | SWT.RESIZE | SWT.DIALOG_TRIM);
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite composite = (Composite) super.createDialogArea(parent);
+ GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
+ composite.setLayoutData(data);
+
+ styledText = new StyledText(composite, SWT.READ_ONLY | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
+ styledText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+ // Create a sightly smaller text (mono-space) font.
+ FontData[] fontData = JFaceResources.getTextFont().getFontData();
+ for (FontData fontDataElement : fontData) {
+ fontDataElement.setHeight(fontDataElement.getHeight() - 1);
+ }
+ Font font = new Font(styledText.getDisplay(), fontData);
+ styledText.setFont(font);
+ styledText.addDisposeListener(e -> font.dispose());
+
+ GC gc = new GC(styledText);
+ gc.setFont(font);
+ data.widthHint = convertWidthInCharsToPixels(gc.getFontMetrics(), 110);
+ gc.dispose();
+
+ update(originalKey, Set.of());
+ return composite;
+ }
+
+ @Override
+ protected void createButtonsForButtonBar(Composite parent) {
+ createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CLOSE_LABEL, true).setFocus();
+ }
+
+ @SuppressWarnings("nls")
+ protected void update(PGPPublicKey key, Set<PGPPublicKey> verifiedCertifications) {
+ StyledString content = new StyledString();
+ String fingerprint = PGPPublicKeyService.toHex(key.getFingerprint()).toUpperCase(Locale.ROOT);
+
+ PublicKeyPacket publicKeyPacket = key.getPublicKeyPacket();
+ publicKeyPacket.getAlgorithm();
+ content.append(" ");
+ content.append(publicKeyPacket instanceof PublicSubkeyPacket ? "sub" : "pub", StyledString.QUALIFIER_STYLER);
+ content.append(" ");
+
+ int algorithm = publicKeyPacket.getAlgorithm();
+ switch (algorithm) {
+ case PublicKeyAlgorithmTags.RSA_GENERAL:
+ case PublicKeyAlgorithmTags.RSA_ENCRYPT:
+ case PublicKeyAlgorithmTags.RSA_SIGN: {
+ content.append("rsa");
+ break;
+ }
+ case PublicKeyAlgorithmTags.DSA: {
+ content.append("dsa");
+ break;
+ }
+ case PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT:
+ case PublicKeyAlgorithmTags.ELGAMAL_GENERAL: {
+ content.append("elgamal");
+ break;
+ }
+ default: {
+ content.append("[");
+ content.append(Integer.toString(algorithm));
+ content.append("]");
+ break;
+ }
+
+ }
+ int bitStrength = key.getBitStrength();
+ content.append(Integer.toString(bitStrength));
+ content.append("/");
+ content.append(fingerprint);
+
+ content.append(" ");
+ content.append(DATE_FORMAT.format(key.getCreationTime()));
+
+ content.append(" ");
+ content.append("\n");
+
+ List<String> users = new ArrayList<>();
+ key.getUserIDs().forEachRemaining(users::add);
+ if (!users.isEmpty()) {
+ for (String user : users) {
+ content.append(" ");
+ content.append("uid", StyledString.QUALIFIER_STYLER);
+ content.append(" ");
+ content.append(user, StyledString.COUNTER_STYLER);
+ content.append("\n");
+ }
+ }
+
+ Long subKeyOf = null;
+
+ for (Iterator<PGPSignature> signatures = key.getSignatures(); signatures.hasNext();) {
+ PGPSignature signature = signatures.next();
+ long keyID = signature.getKeyID();
+
+ if (signature.getSignatureType() == PGPSignature.SUBKEY_BINDING) {
+ subKeyOf = keyID;
+ }
+
+ content.append(" ");
+ content.append("sig", StyledString.QUALIFIER_STYLER);
+ content.append(" ");
+ content.append(PGPPublicKeyService.toHex(keyID));
+ content.append(" ");
+ Date creationTime = signature.getCreationTime();
+ String formattedCreationTime = DATE_FORMAT.format(creationTime);
+ content.append(formattedCreationTime);
+ long signatureExpirationTime = signature.getHashedSubPackets().getSignatureExpirationTime();
+ content.append(" ");
+ content.append(signatureExpirationTime == 0 ? formattedCreationTime.replaceAll(".", "_")
+ : DATE_FORMAT.format(new Date(creationTime.getTime() + 1000 * signatureExpirationTime)));
+
+ content.append(" ");
+ Optional<PGPPublicKey> resolvedKey = verifiedCertifications.stream().filter(k -> k.getKeyID() == keyID)
+ .findFirst();
+
+ long keyExpirationTime = signature.getHashedSubPackets().getKeyExpirationTime();
+ content.append(keyExpirationTime == 0 || resolvedKey == null || !resolvedKey.isPresent()
+ ? formattedCreationTime.replaceAll(".", "_")
+ : DATE_FORMAT.format(
+ new Date(resolvedKey.get().getCreationTime().getTime() + 1000 * keyExpirationTime)));
+
+ if (resolvedKey != null && resolvedKey.isPresent()) {
+ content.append(" ");
+ content.append(getLabel(resolvedKey.get()), StyledString.COUNTER_STYLER);
+ }
+
+ content.append("\n");
+ }
+
+ styledText.setText(content.getString());
+ styledText.setStyleRanges(content.getStyleRanges());
+
+ List<String> title = new ArrayList<>();
+ if (subKeyOf != null) {
+ long keyID = subKeyOf;
+ verifiedCertifications.stream().filter(k -> k.getKeyID() == keyID).findFirst()
+ .ifPresentOrElse(k -> title.add(getLabel(k)), () -> title.add(PGPPublicKeyService.toHex(keyID)));
+ }
+ title.add((subKeyOf == null ? "" : "sub ") + (users.isEmpty() ? fingerprint : users.get(0)));
+
+ setTitle(String.join("\n", title));
+ }
+
+ private String getLabel(PGPPublicKey key) {
+ Iterator<String> userIDs = key.getUserIDs();
+ if (userIDs.hasNext()) {
+ return userIDs.next();
+
+ }
+ return PGPPublicKeyService.toHex(key.getFingerprint()).toUpperCase(Locale.ROOT);
+ }
+
+ private void computeVerifiedCertifications(Shell shell) {
+ Display display = shell.getDisplay();
+ new Job(PGPPublicKeyViewDialog.class.getName()) {
+ {
+ setSystem(true);
+ setPriority(Job.SHORT);
+ }
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ synchronized (keyService) {
+ PGPPublicKey enhancedKey = keyService.addKey(originalKey);
+ Set<PGPPublicKey> verifiedCertifications = keyService.getVerifiedCertifications(originalKey);
+ display.asyncExec(() -> {
+ if (!shell.isDisposed()) {
+ update(enhancedKey, verifiedCertifications);
+ }
+ });
+ }
+ return Status.OK_STATUS;
+ }
+ }.schedule();
+ }
+}
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 26fc363..1d574dc 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
@@ -12,6 +12,7 @@
# IBM Corporation - initial API and implementation
###############################################################################
+PGPPublicKeyViewDialog_Title=PGP Public Key Properties
ProfileModificationAction_InvalidSelections=Problem determining user request. Profile id: {0}, Selection count: {1}
ProfileModificationWizardPage_DetailsLabel=Details
ProfileSnapshots_Label=Installation History