blob: 9c8caf9f9a606f2e20af50fd86b021f58182b4a3 [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 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);
}
}