SignatureVerifier.java

package gov.usgs.earthquake.distribution;

import java.io.File;
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 javax.naming.ConfigurationException;

/** Verifies certificate/key signatures */
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";

  /** Property for verifying signature history based on depth */
  public static final String HISTORY_DEPTH_NAME = "signatureHistoryDepth";

  /** History depth default. Go 3 indices deep in signature history list */
  public static final int HISTORY_DEPTH_DEFAULT = 3;
  /** How far to look in signature history when verifying signatures */
  private int historyDepth = HISTORY_DEPTH_DEFAULT;

  /** 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) {
          this.keychain = ProductKeyChain.fromFile(new File(keychainFileName), KEYCHAIN_PROPERTY_NAME);
        } else {
          LOGGER.warning("[" + getName() + "] no product keys configured");
        }
      }
      String verifyDepth = config.getProperty(HISTORY_DEPTH_NAME);
      if (verifyDepth != null) {
        LOGGER.config("Verifying signature history " + verifyDepth + " entries deep.");
        historyDepth = Integer.parseInt(verifyDepth);
        if (historyDepth <= 0) {
          throw new ConfigurationException(
              "Invalid signatureHistoryDepth configuration. Must be greater than zero.");
        }
      }
    }
  }

  /** @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;
  }

  public Integer getHistoryDepth() {
    return historyDepth;
  }

  public void setHistoryDepth(Integer historyDepth) {
    this.historyDepth = historyDepth;
  }

  /** @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;
          if (product.getSignatureHistory().size() == 0) {
            publicKey = product.verifySignatureKey(candidateKeys, product.getSignatureVersion());
          } else {
            publicKey = product.verifySignatureKey(candidateKeys, product.getSignatureVersion(),
                historyDepth);
          }
          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;
  }

}