blob: b994b7740412b854aa10eb14d26699380eb666bb [file] [log] [blame]
/*******************************************************************************
* 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();
}
}
}