| /******************************************************************************* |
| * 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 java.util.Map.Entry; |
| import org.bouncycastle.bcpg.ArmoredInputStream; |
| import org.bouncycastle.openpgp.*; |
| import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; |
| import org.bouncycastle.openpgp.operator.PGPContentVerifier; |
| import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilder; |
| 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.p2.core.helpers.LogHelper; |
| 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.equinox.p2.repository.artifact.spi.ArtifactDescriptor; |
| import org.eclipse.equinox.p2.repository.spi.PGPPublicKeyService; |
| import org.eclipse.osgi.util.NLS; |
| |
| /** |
| * This processing step verifies PGP signatures are correct (i.e., the 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 and touchpoint |
| * 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 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 PGPPublicKeyService keyService; |
| |
| private IArtifactDescriptor sourceDescriptor; |
| |
| private Map<PGPSignature, List<PGPContentVerifier>> signaturesToVerify = new LinkedHashMap<>(); |
| |
| private Map<PGPContentVerifier, PGPPublicKey> verifierKeys = new LinkedHashMap<>(); |
| |
| private List<OutputStream> signatureVerifiers = new ArrayList<>(); |
| |
| public PGPSignatureVerifier() { |
| super(); |
| link(nullOutputStream(), new NullProgressMonitor()); // this is convenience for tests |
| } |
| |
| 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; |
| } |
| |
| public static PGPPublicKeyStore getKeys(IArtifactDescriptor artifact) { |
| PGPPublicKeyStore keyStore = new PGPPublicKeyStore(); |
| String keyText = artifact.getProperty(PGPSignatureVerifier.PGP_SIGNER_KEYS_PROPERTY_NAME); |
| PGPPublicKeyStore.readPublicKeys(keyText).stream().forEach(keyStore::addKey); |
| return keyStore; |
| } |
| |
| @Override |
| public void initialize(IProvisioningAgent agent, IProcessingStepDescriptor descriptor, |
| IArtifactDescriptor context) { |
| super.initialize(agent, descriptor, context); |
| |
| sourceDescriptor = context; |
| keyService = agent.getService(PGPPublicKeyService.class); |
| |
| // 1. verify declared public keys have signature from a trusted key, if so, add to KeyStore |
| // 2. verify artifact signature matches signature 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; |
| } |
| |
| Collection<PGPSignature> signatures; |
| try { |
| signatures = getSignatures(context); |
| } catch (Exception ex) { |
| setStatus(new Status(IStatus.ERROR, Activator.ID, Messages.Error_CouldNotLoadSignature, ex)); |
| return; |
| } |
| |
| if (signatures.isEmpty()) { |
| setStatus(Status.OK_STATUS); |
| return; |
| } |
| |
| IArtifactRepository repository = context.getRepository(); |
| |
| PGPPublicKeyStore.readPublicKeys(context.getProperty(PGP_SIGNER_KEYS_PROPERTY_NAME)) |
| .forEach(keyService::addKey); |
| if (repository != null) { |
| PGPPublicKeyStore.readPublicKeys(repository.getProperty(PGP_SIGNER_KEYS_PROPERTY_NAME)) |
| .forEach(keyService::addKey); |
| } |
| |
| for (PGPSignature signature : signatures) { |
| long keyID = signature.getKeyID(); |
| Collection<PGPPublicKey> keys = keyService.getKeys(keyID); |
| if (keys.isEmpty()) { |
| LogHelper.log(new Status(IStatus.WARNING, Activator.ID, |
| NLS.bind(Messages.Warning_publicKeyNotFound, PGPPublicKeyService.toHex(keyID), |
| context.getArtifactKey().getId()))); |
| } else { |
| try { |
| PGPContentVerifierBuilder verifierBuilder = new BcPGPContentVerifierBuilderProvider() |
| .get(signature.getKeyAlgorithm(), signature.getHashAlgorithm()); |
| List<PGPContentVerifier> verifiers = new ArrayList<>(); |
| signaturesToVerify.put(signature, verifiers); |
| for (PGPPublicKey key : keys) { |
| PGPContentVerifier verifier = verifierBuilder.build(key); |
| verifierKeys.put(verifier, key); |
| verifiers.add(verifier); |
| signatureVerifiers.add(verifier.getOutputStream()); |
| } |
| } 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) throws IOException { |
| getDestination().write(b); |
| for (OutputStream verifier : signatureVerifiers) { |
| verifier.write(b); |
| } |
| } |
| |
| @Override |
| public void write(byte[] b, int off, int len) throws IOException { |
| getDestination().write(b, off, len); |
| for (OutputStream verifier : signatureVerifiers) { |
| verifier.write(b, off, len); |
| } |
| } |
| |
| @Override |
| public void close() throws IOException { |
| try { |
| if (!getStatus().isOK()) { |
| return; |
| } |
| |
| if (signaturesToVerify.isEmpty()) { |
| return; |
| } |
| |
| PGPPublicKeyStore keyStore = new PGPPublicKeyStore(); |
| for (Entry<PGPSignature, List<PGPContentVerifier>> entry : signaturesToVerify.entrySet()) { |
| PGPSignature signature = entry.getKey(); |
| List<PGPContentVerifier> verifiers = entry.getValue(); |
| boolean verified = false; |
| for (PGPContentVerifier verifier : verifiers) { |
| try { |
| verifier.getOutputStream().write(signature.getSignatureTrailer()); |
| if (verifier.verify(signature.getSignature())) { |
| PGPPublicKey verifyingKey = verifierKeys.get(verifier); |
| if (!Boolean.FALSE.toString() |
| .equalsIgnoreCase(System.getProperty("p2.pgp.verifyExpiration"))) { //$NON-NLS-1$ |
| if (PGPPublicKeyService.compareSignatureTimeToKeyValidityTime(signature, |
| verifyingKey) != 0) { |
| LogHelper.log(new Status(IStatus.WARNING, Activator.ID, |
| NLS.bind(Messages.Error_SignatureAfterKeyExpiration, PGPPublicKeyService |
| .toHexFingerprint(verifyingKey)))); |
| } |
| } |
| |
| if (!Boolean.FALSE.toString() |
| .equalsIgnoreCase(System.getProperty("p2.pgp.verifyRevocation"))) { //$NON-NLS-1$ |
| if (!keyService.isCreatedBeforeRevocation(signature, verifyingKey)) { |
| setStatus(new Status(IStatus.ERROR, Activator.ID, |
| NLS.bind(Messages.Error_SignatureAfterKeyRevocation, PGPPublicKeyService |
| .toHexFingerprint(verifyingKey)))); |
| return; |
| } |
| } |
| |
| keyStore.addKey(verifyingKey); |
| verified = true; |
| break; |
| } |
| } catch (PGPException ex) { |
| LogHelper.log(new Status(IStatus.ERROR, Activator.ID, ex.getMessage(), ex)); |
| } |
| } |
| |
| if (!verified) { |
| setStatus(new Status(IStatus.ERROR, Activator.ID, Messages.Error_SignatureAndFileDontMatch)); |
| return; |
| } |
| } |
| |
| // Update the destination artifact descriptor with the signatures that have been |
| // verified and the keys used for that verification. |
| OutputStream destination = getDestination(); |
| if (destination instanceof IAdaptable) { |
| ArtifactDescriptor destinationDescriptor = ((IAdaptable) destination) |
| .getAdapter(ArtifactDescriptor.class); |
| destinationDescriptor.setProperty(PGP_SIGNATURES_PROPERTY_NAME, |
| sourceDescriptor.getProperty(PGP_SIGNATURES_PROPERTY_NAME)); |
| destinationDescriptor.setProperty(PGP_SIGNER_KEYS_PROPERTY_NAME, keyStore.toArmoredString()); |
| } |
| |
| setStatus(Status.OK_STATUS); |
| } finally |
| |
| { |
| super.close(); |
| } |
| } |
| |
| } |