Source: security/v2/validation-policy-config.js

/**
 * Copyright (C) 2018 Regents of the University of California.
 * @author: Jeff Thompson <[email protected]>
 * @author: From ndn-cxx security https://github.com/named-data/ndn-cxx/blob/master/src/security/v2/validation-policy-config.cpp
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * A copy of the GNU Lesser General Public License is in the file COPYING.
 */

/** @ignore */
var BoostInfoParser = require('../../util/boost-info-parser.js').BoostInfoParser; /** @ignore */
var CertificateRequest = require('./certificate-request.js').CertificateRequest; /** @ignore */
var ValidatorConfigError = require('../validator-config-error.js').ValidatorConfigError; /** @ignore */
var ValidationError = require('./validation-error.js').ValidationError; /** @ignore */
var CertificateV2 = require('./certificate-v2.js').CertificateV2; /** @ignore */
var ConfigRule = require('./validator-config/config-rule.js').ConfigRule; /** @ignore */
var Data = require('../../data.js').Data; /** @ignore */
var Interest = require('../../interest.js').Interest; /** @ignore */
var ValidationPolicy = require('./validation-policy.js').ValidationPolicy;

/**
 * ValidationPolicyConfig implements a validator which can be set up via a
 * configuration file. For command Interest validation, this policy must be
 * combined with ValidationPolicyCommandInterest in order to guard against
 * replay attacks.
 * @note This policy does not support inner policies (a sole policy or a
 * terminal inner policy).
 * See https://named-data.net/doc/ndn-cxx/current/tutorials/security-validator-config.html
 * @constructor
 */
var ValidationPolicyConfig = function ValidationPolicyConfig()
{
  // Call the base constructor.
  ValidationPolicy.call(this);

  this.shouldBypass_ = false;
  this.isConfigured_ = false;
  this.dataRules_ = [];     // of ConfigRule
  this.interestRules_ = []; // of ConfigRule
};

ValidationPolicyConfig.prototype = new ValidationPolicy();
ValidationPolicyConfig.prototype.name = "ValidationPolicyConfig";

exports.ValidationPolicyConfig = ValidationPolicyConfig;

/**
 * There are three forms of load:
 * load(filePath) - Load the configuration from the given config file.
 * load(input, inputName) - Load the configuration from the given input string.
 * load(configSection, inputName) - Load the configuration from the given
 * configSection.
 * Each of these forms of load replaces any existing configuration.
 * @param {String} filePath The The path of the config file.
 * @param {String} input The contents of the configuration rules, with lines
 * separated by "\n" or "\r\n".
 * @param {BoostInfoTree} configSection The configuration section loaded from
 * the config file. It should have one "validator" section.
 * @param {String} inputName Used for log messages, etc.
 */
ValidationPolicyConfig.prototype.load = function
  (filePathOrInputOrConfigSection, inputName)
{
  if (typeof filePathOrInputOrConfigSection === 'string' &&
      inputName == undefined) {
    var filePath = filePathOrInputOrConfigSection;

    var parser = new BoostInfoParser();
    parser.read(filePath);
    this.load(parser.getRoot(), filePath);
  }
  else if (typeof filePathOrInputOrConfigSection === 'string' &&
      typeof inputName === 'string') {
    var input = filePathOrInputOrConfigSection;

    var parser = new BoostInfoParser();
    parser.read(input, inputName);
    this.load(parser.getRoot(), inputName);
  }
  else {
    var configSection = filePathOrInputOrConfigSection;

    if (this.isConfigured_) {
      // Reset the previous configuration.
      this.shouldBypass_ = false;
      this.dataRules_ = [];
      this.interestRules_ = [];

      this.validator_.resetAnchors();
      this.validator_.resetVerifiedCertificates();
    }
    this.isConfigured_ = true;

    var validatorList = configSection.get("validator");
    if (validatorList.length != 1)
      throw new ValidatorConfigError(new Error
        ("ValidationPolicyConfig: Expected one validator section"));
    var validatorSection = validatorList[0];

    // Get the rules.
    var ruleList = validatorSection.get("rule");
    for (var i = 0; i < ruleList.length; ++i) {
      var rule = ConfigRule.create(ruleList[i]);
      if (rule.getIsForInterest())
        this.interestRules_.push(rule);
      else
        this.dataRules_.push(rule);
    }

    // Get the trust anchors.
    var trustAnchorList = validatorSection.get("trust-anchor");
    for (var i = 0; i < trustAnchorList.length; ++i)
      this.processConfigTrustAnchor_(trustAnchorList[i], inputName);
  }
};

/**
 * @param {Data|Interest} dataOrInterest
 * @param {ValidationState} state
 * @param {function} continueValidation
 */
ValidationPolicyConfig.prototype.checkPolicy = function
  (dataOrInterest, state, continueValidation)
{
  if (this.hasInnerPolicy())
    throw new ValidatorConfigError(new Error
      ("ValidationPolicyConfig must be a terminal inner policy"));

  if (this.shouldBypass_) {
    continueValidation(null, state);
    return;
  }

  var keyLocatorName = ValidationPolicy.getKeyLocatorName(dataOrInterest, state);
  if (state.isOutcomeFailed())
    // Already called state.fail() .
    return;

  if (dataOrInterest instanceof Data) {
    var data = dataOrInterest;

    for (var i = 0; i < this.dataRules_.length; ++i) {
      var rule = this.dataRules_[i];

      if (rule.match(false, data.getName())) {
        if (rule.check(false, data.getName(), keyLocatorName, state)) {
          continueValidation
            (new CertificateRequest(new Interest(keyLocatorName)), state);
          return;
        }
        else
          // rule.check failed and already called state.fail() .
          return;
      }
    }

    state.fail(new ValidationError(ValidationError.POLICY_ERROR,
      "No rule matched for data `" + data.getName().toUri() + "`"));
  }
  else {
    var interest = dataOrInterest;

    for (var i = 0; i < this.interestRules_.length; ++i) {
      var rule = this.interestRules_[i];

      if (rule.match(true, interest.getName())) {
        if (rule.check(true, interest.getName(), keyLocatorName, state)) {
          continueValidation
            (new CertificateRequest(new Interest(keyLocatorName)), state);
          return;
        }
        else
          // rule.check failed and already called state.fail() .
          return;
      }
    }

    state.fail(new ValidationError(ValidationError.POLICY_ERROR,
      "No rule matched for interest `" + interest.getName().toUri() + "`"));
  }
};

/**
 * Process the trust-anchor configuration section and call
 * validator_.loadAnchor as needed.
 * @param {BoostInfoTree} configSection The section containing the definition of
 * the trust anchor, e.g. one of "validator.trust-anchor".
 * @param {String} inputName Used for log messages, etc.
 */
ValidationPolicyConfig.prototype.processConfigTrustAnchor_ = function
  (configSection, inputName)
{
  var anchorType = configSection.getFirstValue("type");
  if (anchorType == null)
    throw new ValidatorConfigError(new Error("Expected <trust-anchor.type>"));

  if (anchorType.toLowerCase() == "file") {
    // Get trust-anchor.file .
    var fileName = configSection.getFirstValue("file-name");
    if (fileName == null)
      throw new ValidatorConfigError(new Error("Expected <trust-anchor.file-name>"));

    var refreshPeriod = ValidationPolicyConfig.getRefreshPeriod_(configSection);
    this.validator_.loadAnchor(fileName, fileName, refreshPeriod, false);

    return;
  }
  else if (anchorType.toLowerCase() == "base64") {
    // Get trust-anchor.base64-string .
    var base64String = configSection.getFirstValue("base64-string");
    if (base64String == null)
      throw new ValidatorConfigError(new Error
        ("Expected <trust-anchor.base64-string>"));

    var encoding = new Buffer(base64String, 'base64');
    var certificate = new CertificateV2();
    try {
      certificate.wireDecode(encoding);
    } catch (ex) {
      throw new ValidatorConfigError(new Error
        ("Cannot decode certificate from base64-string: " + ex));
    }
    this.validator_.loadAnchor("", certificate);

    return;
  }
  else if (anchorType.toLowerCase() == "dir") {
    // Get trust-anchor.dir .
    var dirString = configSection.getFirstValue("dir");
    if (dirString == null)
      throw new ValidatorConfigError(new Error("Expected <trust-anchor.dir>"));

    var refreshPeriod = ValidationPolicyConfig.getRefreshPeriod_(configSection);
    this.validator_.loadAnchor(dirString, dirString, refreshPeriod, true);

    return;
  }
  else if (anchorType.toLowerCase() == "any")
    this.shouldBypass_ = true;
  else
    throw new ValidatorConfigError(new Error("Unsupported trust-anchor.type"));
};

/**
 * Get the "refresh" value. If the value is 9, return a period of one hour.
 * @param {BoostInfoTree} configSection The section containing the definition of
 * the trust anchor, e.g. one of "validator.trust-anchor".
 * @return {number} The refresh period in milliseconds. However if there is no
 * "refresh" value, return a large number (effectively no refresh).
 */
ValidationPolicyConfig.getRefreshPeriod_ = function(configSection)
{
  var refreshString = configSection.getFirstValue("refresh");
  if (refreshString == null)
    // Return a large value (effectively no refresh).
    return 1e14;

  var refreshSeconds = 0.0;
  var refreshMatch = refreshString.match(/(\d+)([hms])/);;
  if (refreshMatch != null) {
    refreshSeconds = parseInt(refreshMatch[1]);
    if (refreshMatch[2] != 's') {
      refreshSeconds *= 60;
      if (refreshMatch[2] != 'm')
        refreshSeconds *= 60;
    }
  }

  if (refreshSeconds == 0.0)
    // Use an hour instead of 0.
    return 3600 * 1000.0;
  else
    // Convert from seconds to milliseconds.
    return refreshSeconds * 1000.0;
};