| /******************************************************************************* |
| * Copyright (c) 2021 Red Hat Inc. 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.artifact.processors.pgp; |
| |
| import java.io.*; |
| import java.nio.charset.StandardCharsets; |
| import java.util.*; |
| import org.bouncycastle.bcpg.ArmoredInputStream; |
| import org.bouncycastle.openpgp.*; |
| import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; |
| import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; |
| import org.eclipse.core.runtime.*; |
| import org.eclipse.equinox.internal.p2.artifact.repository.Activator; |
| import org.eclipse.equinox.internal.provisional.p2.artifact.repository.processing.ProcessingStep; |
| import org.eclipse.equinox.p2.core.IProvisioningAgent; |
| import org.eclipse.equinox.p2.repository.artifact.*; |
| import org.eclipse.osgi.util.NLS; |
| |
| /** |
| * This processing step verifies PGP signatures are correct (ie artifact was not |
| * tampered during fetch). Note that is does <b>not</b> deal with trust. Dealing |
| * with trusted signers is done as part of CheckTrust touchpoint and phase. |
| */ |
| public final class PGPSignatureVerifier extends ProcessingStep { |
| |
| /** |
| * ID of the registering |
| * <code>org.eclipse.equinox.p2.artifact.repository.processingSteps</tt> |
| * extension. |
| */ |
| public static final String ID = "org.eclipse.equinox.p2.processing.PGPSignatureCheck"; //$NON-NLS-1$ |
| |
| public static final PGPPublicKeyStore KNOWN_KEYS = new PGPPublicKeyStore(); |
| |
| 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 Collection<PGPSignature> signaturesToVerify; |
| |
| public PGPSignatureVerifier() { |
| super(); |
| link(nullOutputStream(), new NullProgressMonitor()); // this is convenience for tests |
| } |
| |
| public static Map<PGPPublicKey, Set<PGPPublicKey>> getVerifiedKnownKeyCertifications() { |
| Map<PGPPublicKey, Set<PGPPublicKey>> result = new LinkedHashMap<>(); |
| for (PGPPublicKey key : KNOWN_KEYS.all()) { |
| Set<PGPPublicKey> certifications = new LinkedHashSet<>(); |
| for (Iterator<PGPSignature> signatures = key.getSignatures(); signatures.hasNext();) { |
| PGPSignature signature = signatures.next(); |
| long signingKeyID = signature.getKeyID(); |
| PGPPublicKey signingKey = KNOWN_KEYS.getKey(signingKeyID); |
| if (signingKey != null) { |
| switch (signature.getSignatureType()) { |
| case PGPSignature.SUBKEY_BINDING: |
| case PGPSignature.PRIMARYKEY_BINDING: { |
| try { |
| signature.init(new BcPGPContentVerifierBuilderProvider(), signingKey); |
| if (signature.verifyCertification(signingKey, key)) { |
| certifications.add(signingKey); |
| } |
| } catch (PGPException e) { |
| //$FALL-THROUGH$ |
| } |
| break; |
| } |
| case PGPSignature.DEFAULT_CERTIFICATION: |
| case PGPSignature.NO_CERTIFICATION: |
| case PGPSignature.CASUAL_CERTIFICATION: |
| case PGPSignature.POSITIVE_CERTIFICATION: { |
| for (Iterator<String> userIDs = key.getUserIDs(); userIDs.hasNext();) { |
| String userID = userIDs.next(); |
| try { |
| signature.init(new BcPGPContentVerifierBuilderProvider(), signingKey); |
| if (signature.verifyCertification(userID, key)) { |
| certifications.add(signingKey); |
| break; |
| } |
| } catch (PGPException e) { |
| //$FALL-THROUGH$ |
| } |
| } |
| break; |
| } |
| } |
| } |
| } |
| result.put(key, certifications); |
| } |
| return result; |
| } |
| |
| public static Collection<PGPSignature> getSignatures(IArtifactDescriptor artifact) |
| throws IOException, PGPException { |
| String signatureText = unnormalizedPGPProperty(artifact.getProperty(PGP_SIGNATURES_PROPERTY_NAME)); |
| if (signatureText == null) { |
| return Collections.emptyList(); |
| } |
| List<PGPSignature> res = new ArrayList<>(); |
| try (InputStream in = new ArmoredInputStream( |
| new ByteArrayInputStream(signatureText.getBytes(StandardCharsets.US_ASCII)))) { |
| PGPObjectFactory pgpFactory = new BcPGPObjectFactory(in); |
| Object o = pgpFactory.nextObject(); |
| PGPSignatureList signatureList = new PGPSignatureList(new PGPSignature[0]); |
| if (o instanceof PGPCompressedData) { |
| PGPCompressedData pgpCompressData = (PGPCompressedData) o; |
| pgpFactory = new BcPGPObjectFactory(pgpCompressData.getDataStream()); |
| signatureList = (PGPSignatureList) pgpFactory.nextObject(); |
| } else if (o instanceof PGPSignatureList) { |
| signatureList = (PGPSignatureList) o; |
| } |
| signatureList.iterator().forEachRemaining(res::add); |
| } |
| return res; |
| } |
| |
| @Override |
| public void initialize(IProvisioningAgent agent, IProcessingStepDescriptor descriptor, |
| IArtifactDescriptor context) { |
| 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; |
| } |
| if (signaturesToVerify.isEmpty()) { |
| setStatus(Status.OK_STATUS); |
| return; |
| } |
| |
| IArtifactRepository repository = context.getRepository(); |
| KNOWN_KEYS.addKeys(context.getProperty(PGP_SIGNER_KEYS_PROPERTY_NAME), |
| repository != null ? repository.getProperty(PGP_SIGNER_KEYS_PROPERTY_NAME) : null); |
| for (PGPSignature signature : signaturesToVerify) { |
| PGPPublicKey publicKey = KNOWN_KEYS.getKey(signature.getKeyID()); |
| if (publicKey == null) { |
| setStatus(new Status(IStatus.ERROR, Activator.ID, |
| NLS.bind(Messages.Error_publicKeyNotFound, Long.toHexString(signature.getKeyID())))); |
| return; |
| } |
| try { |
| signature.init(new BcPGPContentVerifierBuilderProvider(), publicKey); |
| } catch (PGPException ex) { |
| setStatus(new Status(IStatus.ERROR, Activator.ID, ex.getMessage(), ex)); |
| return; |
| } |
| } |
| } |
| |
| /** |
| * See // https://www.w3.org/TR/1998/REC-xml-19980210#AVNormalize, newlines |
| * replaced by spaces by parser, needs to be restored |
| * |
| * @param armoredPGPBlock the PGP block, in armored form |
| * @return fixed PGP armored blocks |
| */ |
| static String unnormalizedPGPProperty(String armoredPGPBlock) { |
| if (armoredPGPBlock == null) { |
| return null; |
| } |
| if (armoredPGPBlock.contains("\n") || armoredPGPBlock.contains("\r")) { //$NON-NLS-1$ //$NON-NLS-2$ |
| return armoredPGPBlock; |
| } |
| return armoredPGPBlock.replace(' ', '\n') |
| .replace("-----BEGIN\nPGP\nSIGNATURE-----", "-----BEGIN PGP SIGNATURE-----") //$NON-NLS-1$ //$NON-NLS-2$ |
| .replace("-----END\nPGP\nSIGNATURE-----", "-----END PGP SIGNATURE-----") //$NON-NLS-1$ //$NON-NLS-2$ |
| .replace("-----BEGIN\nPGP\nPUBLIC\nKEY\nBLOCK-----", "-----BEGIN PGP PUBLIC KEY BLOCK-----") //$NON-NLS-1$ //$NON-NLS-2$ |
| .replace("-----END\nPGP\nPUBLIC\nKEY\nBLOCK-----", "-----END PGP PUBLIC KEY BLOCK-----"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| @Override |
| public void write(int b) { |
| if (signaturesToVerify != null) { |
| signaturesToVerify.iterator().forEachRemaining(signature -> signature.update((byte) b)); |
| } |
| |
| } |
| |
| @Override |
| public void write(byte[] b) throws IOException { |
| getDestination().write(b); |
| if (signaturesToVerify != null) { |
| signaturesToVerify.iterator().forEachRemaining(signature -> signature.update(b)); |
| } |
| } |
| |
| @Override |
| public void write(byte[] b, int off, int len) throws IOException { |
| getDestination().write(b, off, len); |
| if (signaturesToVerify != null) { |
| signaturesToVerify.iterator().forEachRemaining(signature -> signature.update(b, off, len)); |
| } |
| } |
| |
| @Override |
| public void close() { |
| if (!getStatus().isOK()) { |
| return; |
| } |
| if (signaturesToVerify == null || signaturesToVerify.isEmpty()) { |
| return; |
| } |
| Iterator<PGPSignature> iterator = signaturesToVerify.iterator(); |
| while (iterator.hasNext()) { |
| PGPSignature signature = iterator.next(); |
| try { |
| if (!signature.verify()) { |
| setStatus(new Status(IStatus.ERROR, Activator.ID, Messages.Error_SignatureAndFileDontMatch)); |
| return; |
| } |
| } catch (PGPException ex) { |
| setStatus(new Status(IStatus.ERROR, Activator.ID, ex.getMessage(), ex)); |
| return; |
| } |
| } |
| setStatus(Status.OK_STATUS); |
| } |
| |
| } |