SignatureVerifier.java

package gov.usgs.earthquake.distribution;

import java.io.File;
import java.io.InputStream;
import java.security.PublicKey;
import java.util.Optional;
import java.util.logging.Logger;

import gov.usgs.earthquake.product.Product;
import gov.usgs.earthquake.product.ProductId;
import gov.usgs.util.Config;
import gov.usgs.util.DefaultConfigurable;
import gov.usgs.util.StreamUtils;

public class SignatureVerifier extends DefaultConfigurable {

	/** logging object. */
	private static final Logger LOGGER = Logger
			.getLogger(SignatureVerifier.class.getName());

	/** Property for whether or not to verify signatures. */
	public static final String VERIFY_SIGNATURES_PROPERTY_NAME = "verifySignatures";
	/** Don't verify signatures (Default). */
	public static final String DEFAULT_VERIFY_SIGNATURE = "off";
	/** Test signatures, but don't reject invalid. */

	public static final String TEST_VERIFY_SIGNATURE = "test";
	/** Allow products that do not have a configured key. */
	public static final String ONLY_VERIFY_KNOWN = "allowUnknownSigner";

	/** Property for a list of keys. */
	public static final String KEYCHAIN_PROPERTY_NAME = "keychain";

	/** Property for a file of keys. */
	public static final String KEYCHAIN_FILE_PROPERTY_NAME = "keychainFile";

	/** Whether or not to reject invalid signatures. */
	private boolean rejectInvalidSignatures = false;

	/** If not rejecting invalid signatures, test them anyways. */
	private boolean testSignatures = false;

	/**
	 * When rejecting invalid signatures, true will prevent an
	 * InvalidSignatureException if there are no candidate keys.
	 */
	private boolean allowUnknownSigner = false;

	/** List of candidate keys. */
	private ProductKeyChain keychain;

	@Override
	public void configure(final Config config) throws Exception {
		String verifySignatures = config
				.getProperty(VERIFY_SIGNATURES_PROPERTY_NAME);
		// configured
		if (verifySignatures != null) {
			// "test"
			if (verifySignatures.equals(TEST_VERIFY_SIGNATURE)) {
				testSignatures = true;
				LOGGER.config("[" + getName() + "] test message signatures");
			}
			// not "off"
			else if (!verifySignatures.equals(DEFAULT_VERIFY_SIGNATURE)) {
				rejectInvalidSignatures = true;
				LOGGER.config("[" + getName() + "] reject invalid signatures");
			}

			String keyNames = config.getProperty(KEYCHAIN_PROPERTY_NAME);
			if (keyNames != null) {
				LOGGER.config("[" + getName() + "] using product keys "
						+ keyNames);
				keychain = new ProductKeyChain(keyNames, Config.getConfig());
			} else {
				String keychainFileName = config.getProperty(KEYCHAIN_FILE_PROPERTY_NAME);
				if (keychainFileName != null) {
					Config keychainConfig = new Config();

					try (InputStream in = StreamUtils.getInputStream(
							new File(keychainFileName))) {
						keychainConfig.load(in);
					}
					keyNames = keychainConfig.getProperty(KEYCHAIN_PROPERTY_NAME);
					keychain = new ProductKeyChain(keyNames,keychainConfig);
				} else {
					LOGGER.warning("[" + getName() + "] no product keys configured");
				}
			}
		}
	}

	/** @return boolean RejectInvalidSignatures */
	public boolean isRejectInvalidSignatures() {
		return rejectInvalidSignatures;
	}

	/** @param rejectInvalidSignatures boolean to set */
	public void setRejectInvalidSignatures(boolean rejectInvalidSignatures) {
		this.rejectInvalidSignatures = rejectInvalidSignatures;
	}

	/** @return boolean TestSignatures */
	public boolean isTestSignatures() {
		return testSignatures;
	}

	/** @param testSignatures boolean to set */
	public void setTestSignatures(boolean testSignatures) {
		this.testSignatures = testSignatures;
	}

	/** @return Product keychain */
	public ProductKeyChain getKeychain() {
		return keychain;
	}

	/** @param keychain ProductKeyChain to set */
	public void setKeychain(ProductKeyChain keychain) {
		this.keychain = keychain;
	}

	/** @return boolean AllowUnknownSigner */
	public boolean isAllowUnknownSigner() {
		return allowUnknownSigner;
	}

	/** @param allowUnknownSigner boolean to set */
	public void setAllowUnknownSigner(boolean allowUnknownSigner) {
		this.allowUnknownSigner = allowUnknownSigner;
	}

	/**
	 * Attempt to verify a products signature.
	 *
	 * @param product
	 *            product to verify.
	 * @return true if the signature is from a key in the keychain.
	 * @throws InvalidSignatureException
	 *             if rejectInvalidSignatures=true, and signature was not
	 *             verified; allowUnknownSigner=true prevents this exception
	 *             when no keys are found in the keychain for the product.
	 * @throws Exception if error occurs
	 */
	public boolean verifySignature(final Product product) throws Exception {
		boolean verified = false;
		String verifiedKeyName = null;

		if (testSignatures || rejectInvalidSignatures) {
			ProductId id = product.getId();
			PublicKey[] candidateKeys = new PublicKey[] {};

			if (keychain != null) {
				candidateKeys = keychain.getProductKeys(id);
				LOGGER.finer("[" + getName() + "] number of candidate keys="
						+ candidateKeys.length);
				if (candidateKeys.length > 0) {
					PublicKey publicKey = product.verifySignatureKey(
							candidateKeys,
							product.getSignatureVersion());
					if (publicKey != null) {
						verified = true;
						// find key that verified
						Optional<ProductKey> verifiedKey = keychain.getKeychain()
								.stream().filter(key -> {
									return publicKey.equals(key.getKey());
								}).findAny();
						if (verifiedKey.isPresent()) {
							verifiedKeyName = verifiedKey.get().getName();
						}
					}
				}
			} else {
				LOGGER.warning("[" + getName() + "] missing Signature Keychain");
			}

			LOGGER.fine("[" + getName() + "] signature verified=" + verified
					+ (verified ? " (key=" + verifiedKeyName + ")" : "")
					+ ", id=" + product.getId());

			if (allowUnknownSigner && candidateKeys.length == 0) {
					LOGGER.finer("[" + getName()
							+ "] unknown signer, allowed by configuration");
					return false;
			}

			if (!verified && rejectInvalidSignatures) {
				throw new InvalidSignatureException("[" + getName()
						+ "] bad signature for id=" + id);
			}
		}

		return verified;
	}

}