diff options
author | Mickael Istria | 2021-04-13 20:42:05 +0000 |
---|---|---|
committer | Mickael Istria | 2021-06-15 13:34:02 +0000 |
commit | 45af7098437c45a46e7637d9aeb0177ca998e209 (patch) | |
tree | 0d50d16f52f6f79fc49979661462666823c91f37 | |
parent | ddd6fcd94de359b9861685fa325fe8778398c691 (diff) | |
download | rt.equinox.p2-45af7098437c45a46e7637d9aeb0177ca998e209.tar.gz rt.equinox.p2-45af7098437c45a46e7637d9aeb0177ca998e209.tar.xz rt.equinox.p2-45af7098437c45a46e7637d9aeb0177ca998e209.zip |
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>
28 files changed, 563 insertions, 201 deletions
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 6c49a76b0..6023a8c18 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 @@ Manifest-Version: 1.0 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 @@ Export-Package: org.eclipse.equinox.internal.p2.artifact.processing;x-friends:=" 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 4826e17d2..3e4f3e6cd 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 e81fc0e43..38439d650 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 @@ package org.eclipse.equinox.internal.p2.artifact.processors.pgp; 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,43 +41,53 @@ public final class PGPSignatureVerifier extends ProcessingStep { */ 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 } - @Override - public void initialize(IProvisioningAgent agent, IProcessingStepDescriptor descriptor, - IArtifactDescriptor context) { - super.initialize(agent, descriptor, context); -// 1. verify declared public keys have signature from a trusted key, if so, add to KeyStore -// 2. verify artifact signature matches signture of given keys, and at least 1 of this key is trusted - String signatureText = unnormalizedPGPProperty(context.getProperty(PGP_SIGNATURES_PROPERTY_NAME)); + private static Collection<PGPSignature> getSignatures(IArtifactDescriptor artifact) + throws IOException, PGPException { + String signatureText = unnormalizedPGPProperty(artifact.getProperty(PGP_SIGNATURES_PROPERTY_NAME)); if (signatureText == null) { - setStatus(Status.OK_STATUS); - return; + return Collections.emptyList(); } - signaturesToVerify = new ArrayList<>(); + 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; + 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; - } else { - setStatus(new Status(IStatus.ERROR, Activator.ID, - Messages.Error_CouldNotLoadSignature)); - return; } - signatureList.iterator().forEachRemaining(signaturesToVerify::add); + signatureList.iterator().forEachRemaining(res::add); + } + return res; + } + + @Override + public void initialize(IProvisioningAgent agent, IProcessingStepDescriptor descriptor, + IArtifactDescriptor context) { + super.initialize(agent, descriptor, context); +// 1. verify declared public keys have signature from a trusted key, if so, add to KeyStore +// 2. verify artifact signature matches signture of given keys, and at least 1 of this key is trusted + String signatureText = unnormalizedPGPProperty(context.getProperty(PGP_SIGNATURES_PROPERTY_NAME)); + if (signatureText == null) { + setStatus(Status.OK_STATUS); + return; + } + try { + signaturesToVerify = getSignatures(context); } catch (Exception ex) { setStatus(new Status(IStatus.ERROR, Activator.ID, Messages.Error_CouldNotLoadSignature, ex)); return; @@ -87,10 +98,10 @@ public final class PGPSignatureVerifier extends ProcessingStep { } 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 @@ public final class PGPSignatureVerifier extends ProcessingStep { * @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 @@ public final class PGPSignatureVerifier extends ProcessingStep { .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 @@ public final class PGPSignatureVerifier extends ProcessingStep { } @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 @@ public final class PGPSignatureVerifier extends ProcessingStep { } 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 965277d1c..d9f4dfe22 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.net.URISyntaxException; 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 @@ public class MirrorRequest extends ArtifactRequest { 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 @@ public class MirrorRequest extends ArtifactRequest { 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 @@ public class MirrorRequest extends ArtifactRequest { 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 @@ public class MirrorRequest extends ArtifactRequest { ((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 d8b8128f1..19cf0c2f6 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 @@ Manifest-Version: 1.0 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 @@ Export-Package: org.eclipse.equinox.internal.p2.core;x-friends:="org.eclipse.equ 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 34dd2a2a1..3cac38cdd 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 2e6e2d97b..f2e27a243 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 abstract class UIServices { */ 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 @@ public abstract class UIServices { } /** + * + * @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 abstract class UIServices { 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 fdc95836c..83eacd2af 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 @@ Export-Package: org.eclipse.equinox.internal.p2.engine; 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-RequiredExecutionEnvironment: JavaSE-11 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 6fea6e340..8b91ed338 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 @@ public class InstallableUnitOperand extends Operand { * 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 @@ public class InstallableUnitOperand extends Operand { 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 7e1e729a8..964072ce8 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.io.IOException; 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.framework.ServiceReference; 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 class CertificateChecker { 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 @@ public class CertificateChecker { 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 @@ public class CertificateChecker { } 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 @@ public class CertificateChecker { // 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 class CertificateChecker { } - 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 531f95331..e885ad1f6 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.PhaseSetFactory; 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 @@ import org.eclipse.equinox.p2.metadata.ITouchpointType; */ 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<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 @@ public class CheckTrust extends InstallableUnitPhase { @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 @@ public class CheckTrust extends InstallableUnitPhase { @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 bd2e3e2ad..595a6239f 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 @@ package org.eclipse.equinox.p2.tests.artifact.repository; 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 79c58faf5..dd7cc84ad 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 @@ package org.eclipse.equinox.p2.tests.engine; 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 @@ public class CertificateCheckerTest extends AbstractProvisioningTest { //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 class CertificateCheckerTest extends AbstractProvisioningTest { 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 @@ public class CertificateCheckerTest extends AbstractProvisioningTest { 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 class CertificateCheckerTest extends AbstractProvisioningTest { 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 @@ public class CertificateCheckerTest extends AbstractProvisioningTest { 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 @@ public class CertificateCheckerTest extends AbstractProvisioningTest { 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 @@ public class CertificateCheckerTest extends AbstractProvisioningTest { 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 fbc3fc5f4..b1cade2a0 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 @@ public class CheckTrustActionTest extends AbstractProvisioningTest { 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 @@ public class CheckTrustActionTest extends AbstractProvisioningTest { 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 000000000..68b911bfa --- /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='(& (classifier=osgi.bundle))' output='${repoUrl}/plugins/${id}_${version}.jar'/> + <rule filter='(& (classifier=binary))' output='${repoUrl}/binary/${id}_${version}'/> + <rule filter='(& (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 000000000..db8f67ba2 --- /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
Bundle-Version: 1.0.0.qualifier
 + </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 Binary files differnew file mode 100644 index 000000000..81573b2aa --- /dev/null +++ b/bundles/org.eclipse.equinox.p2.tests/testData/CertificateChecker/selfsigned/plugins/blah_1.0.0.123456.jar 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 5c3932fcc..71901cee2 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 @@ Manifest-Version: 1.0 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-RequiredExecutionEnvironment: JavaSE-11 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 7fc6bae69..12d492815 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 ee394cfcf..ec918b53c 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 class ActionConstants { 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 201535517..7c7b51c17 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.Collection; 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.engine.spi.ProvisioningAction; 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 @@ public class CheckTrustAction extends ProvisioningAction { 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 270399d37..f5f8f384b 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 @@ Manifest-Version: 1.0 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 @@ Require-Bundle: org.eclipse.ui;bundle-version="3.107.0", 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 @@ Import-Package: javax.xml.parsers, 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 5954a5e0b..770c5b81a 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 07ad9b6c1..bcee1d0ef 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 class ProvUIMessages extends NLS { 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 1c9135553..482fe4844 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 @@ package org.eclipse.equinox.internal.p2.ui; 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.swt.widgets.*; 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 @@ public class ValidationDialogServiceUI extends UIServices { */ 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 @@ public class ValidationDialogServiceUI extends UIServices { 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 @@ public class ValidationDialogServiceUI extends UIServices { @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 @@ public class ValidationDialogServiceUI extends UIServices { }); 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); - - // 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); + return new TrustInfo(trustedCertificates, trustedKeys, persistTrust, trustUnsigned); + // 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 @@ public class ValidationDialogServiceUI extends UIServices { 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 @@ public class ValidationDialogServiceUI extends UIServices { 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 460592263..a37911a92 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.CertificateEncodingException; 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 @@ public class TrustCertificateDialog extends SelectionDialog { 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 @@ public class TrustCertificateDialog extends SelectionDialog { listViewer.addDoubleClickListener(getDoubleClickListener()); listViewer.addSelectionChangedListener(getParentSelectionListener()); createButtons(composite); + detailsButton.setEnabled(selectedCertificate instanceof X509Certificate); return composite; } @@ -140,11 +167,12 @@ public class TrustCertificateDialog extends SelectionDialog { 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 @@ public class TrustCertificateDialog extends SelectionDialog { 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 class TrustCertificateDialog extends SelectionDialog { public void widgetSelected(SelectionEvent e) { widgetDefaultSelected(e); } + }); } @@ -177,8 +221,29 @@ public class TrustCertificateDialog extends SelectionDialog { 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 @@ public class TrustCertificateDialog extends SelectionDialog { 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 8dd78ac93..561551af2 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 @@ RollbackProfileElement_CurrentInstallation=Current Installation 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 16246ed34..9eb67d01e 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 @@ public class CertificateLabelProvider implements ILabelProvider { @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 |