Source: security/verification-helpers.js

/**
 * Copyright (C) 2017-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/verification-helpers.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 Crypto = require('../crypto.js'); /** @ignore */
var SyncPromise = require('../util/sync-promise.js').SyncPromise; /** @ignore */
var Blob = require('../util/blob.js').Blob; /** @ignore */
var WireFormat = require('../encoding/wire-format.js').WireFormat; /** @ignore */
var KeyType = require('./security-types.js').KeyType; /** @ignore */
var DigestAlgorithm = require('./security-types.js').DigestAlgorithm; /** @ignore */
var UseSubtleCrypto = require("../use-subtle-crypto-node.js").UseSubtleCrypto; /** @ignore */
var CertificateV2 = require('./v2/certificate-v2.js').CertificateV2; /** @ignore */
var PublicKey = require('./certificate/public-key.js').PublicKey;

/**
 * The VerificationHelpers class has static methods to verify signatures and
 * digests.
 */
var VerificationHelpers = function VerificationHelpers() {};

exports.VerificationHelpers = VerificationHelpers;

/**
 * Verify the buffer against the signature using the public key.
 * @param {Buffer|Blob} buffer The input buffer to verify.
 * @param {Buffer|Blob} signature The signature bytes.
 * @param {PublicKey|Buffer|Blob} publicKey The object containing the public key,
 * or the public key DER which is used to make the PublicKey object.
 * @param {number} digestAlgorithm (optional) The digest algorithm as an int
 * from the DigestAlgorithm enum. If omitted, use DigestAlgorithm.SHA256.
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which returns true if verification
 * succeeds, false if verification fails, or a promise rejected with Error for
 * an invalid public key type or digestAlgorithm.
 */
VerificationHelpers.verifySignaturePromise = function
  (buffer, signature, publicKey, digestAlgorithm, useSync)
{
  if (typeof digestAlgorithm === 'boolean') {
    // digestAlgorithm is omitted, so shift.
    useSync = digestAlgorithm;
    digestAlgorithm = undefined;
  }

  if (buffer instanceof Blob)
    buffer = buffer.buf();
  if (signature instanceof Blob)
    signature = signature.buf();
  if (!(publicKey instanceof PublicKey)) {
    // Turn publicKey into a PublicKey object.
    try {
      if (!(publicKey instanceof Blob))
        publicKey = new Blob(publicKey);
      publicKey = new PublicKey(publicKey);
    } catch (ex) {
      return SyncPromise.reject(new Error
        ("verifySignaturePromise: Error decoding public key DER: " + ex));
    }
  }
  if (digestAlgorithm == undefined)
    digestAlgorithm = DigestAlgorithm.SHA256;

  if (digestAlgorithm == DigestAlgorithm.SHA256) {
    if (publicKey.getKeyType() == KeyType.RSA) {
      if (UseSubtleCrypto() && !useSync) {
        var algo = {name:"RSASSA-PKCS1-v1_5", hash:{name:"SHA-256"}};

        return crypto.subtle.importKey
          ("spki", publicKey.getKeyDer().buf().buffer, algo, true, ["verify"])
        .then(function(key) {
          return crypto.subtle.verify(algo, key, signature, buffer)
        });
      }
      else {
        try {
          if (VerificationHelpers.verifyUsesString_ === null)
            VerificationHelpers.setVerifyUsesString_();

          // The crypto verifier requires a PEM-encoded public key.
          var keyBase64 = publicKey.getKeyDer().buf().toString('base64');
          var keyPem = "-----BEGIN PUBLIC KEY-----\n";
          for (var i = 0; i < keyBase64.length; i += 64)
            keyPem += (keyBase64.substr(i, 64) + "\n");
          keyPem += "-----END PUBLIC KEY-----";

          var verifier = Crypto.createVerify('RSA-SHA256');
          verifier.update(buffer);
          var signatureBytes = VerificationHelpers.verifyUsesString_ ?
            signature.toString('binary') : signature;
          return SyncPromise.resolve(verifier.verify(keyPem, signatureBytes));
        } catch (ex) {
          return SyncPromise.reject(new Error
            ("verifySignaturePromise: Error is RSA verify: " + ex));
        }
      }
    }
    else if (publicKey.getKeyType() == KeyType.EC) {
      try {
        if (VerificationHelpers.verifyUsesString_ === null)
          VerificationHelpers.setVerifyUsesString_();

        // The crypto verifier requires a PEM-encoded public key.
        var keyBase64 =  publicKey.getKeyDer().buf().toString("base64");
        var keyPem = "-----BEGIN PUBLIC KEY-----\n";
        for (var i = 0; i < keyBase64.length; i += 64)
          keyPem += (keyBase64.substr(i, 64) + "\n");
        keyPem += "-----END PUBLIC KEY-----";

        // Just create a "sha256". The Crypto library will infer ECDSA from the key.
        var verifier = Crypto.createVerify("sha256");
        verifier.update(buffer);
        var signatureBytes = VerificationHelpers.verifyUsesString_ ?
          signature.toString('binary') : signature;
        return SyncPromise.resolve(verifier.verify(keyPem, signatureBytes));
      } catch (ex) {
        return SyncPromise.reject(new Error
          ("verifySignaturePromise: Error is ECDSA verify: " + ex));
      }
    }
    else
      return SyncPromise.reject(new Error("verifySignaturePromise: Invalid key type"));
  }
  else
    return SyncPromise.reject(new Error
      ("verifySignaturePromise: Invalid digest algorithm"));
};

/**
 * Verify the buffer against the signature using the public key.
 * @param {Buffer|Blob} buffer The input buffer to verify.
 * @param {Buffer|Blob} signature The signature bytes.
 * @param {PublicKey|Buffer|Blob} publicKey The object containing the public key,
 * or the public key DER which is used to make the PublicKey object.
 * @param {number} digestAlgorithm (optional) The digest algorithm as an int
 * from the DigestAlgorithm enum. If omitted, use DigestAlgorithm.SHA256.
 * @param {function} onComplete (optional) This calls
 * onComplete(result) with true if verification succeeds, false if verification
 * fails. If omitted, the return value is described below. (Some crypto
 * libraries only use a callback, so onComplete is required to use these.)
 * NOTE: The library will log any exceptions thrown by this callback, but for
 * better error handling the callback should catch and properly handle any
 * exceptions.
 * @param {function} onError (optional) If defined, then onComplete must be
 * defined and if there is an exception, then this calls onError(exception)
 * with the exception. If onComplete is defined but onError is undefined, then
 * this will log any thrown exception. (Some crypto libraries only use a
 * callback, so onError is required to be notified of an exception.)
 * NOTE: The library will log any exceptions thrown by this callback, but for
 * better error handling the callback should catch and properly handle any
 * exceptions.
 * @return {boolean} If onComplete is omitted, return true if verification
 * succeeds, false if verification fails. Otherwise, if onComplete is supplied
 * then return undefined and use onComplete as described above.
 * @throws Error for an invalid public key type or digestAlgorithm. However, if
 * onComplete and onError are defined, then if there is an exception return
 * undefined and call onError(exception).
 */
VerificationHelpers.verifySignature = function
  (buffer, signature, publicKey, digestAlgorithm, onComplete, onError)
{
  if (typeof digestAlgorithm === 'function') {
    // digestAlgorithm is omitted, so shift.
    onError = onComplete;
    onComplete = digestAlgorithm;
    digestAlgorithm = undefined;
  }

  return SyncPromise.complete(onComplete, onError,
    this.verifySignaturePromise
      (buffer, signature, publicKey, digestAlgorithm, !onComplete));
};

/**
 * Verify the Data packet using the public key. This does not check the type of
 * public key or digest algorithm against the type of SignatureInfo in the Data
 * packet such as Sha256WithRsaSignature.
 * @param {Data} data The Data packet to verify.
 * @param {PublicKey|Buffer|Blob|CertificateV2} publicKeyOrCertificate The
 * object containing the public key, or the public key DER which is used to make
 * the PublicKey object, or the certificate containing the public key.
 * @param {number} digestAlgorithm (optional) The digest algorithm as an int
 * from the DigestAlgorithm enum. If omitted, use DigestAlgorithm.SHA256.
 * @param {WireFormat} wireFormat (optional) A WireFormat object used to encode
 * the Data packet. If omitted, use WireFormat getDefaultWireFormat().
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which returns true if verification
 * succeeds, false if verification fails, or a promise rejected with Error for
 * an invalid public key type or digestAlgorithm.
 */
VerificationHelpers.verifyDataSignaturePromise = function
  (data, publicKeyOrCertificate, digestAlgorithm, wireFormat, useSync)
{
  var arg3 = digestAlgorithm;
  var arg4 = wireFormat;
  var arg5 = useSync;
  // arg3,            arg4,       arg5
  // digestAlgorithm, wireFormat, useSync
  // digestAlgorithm, wireFormat, null
  // digestAlgorithm, useSync,    null
  // digestAlgorithm, null,       null
  // wireFormat,      useSync,    null
  // wireFormat,      null,       null
  // useSync,         null,       null
  // null,            null,       null
  if (typeof arg3 === 'number')
    digestAlgorithm = arg3;
  else
    digestAlgorithm = undefined;

  if (arg3 instanceof WireFormat)
    wireFormat = arg3;
  else if (arg4 instanceof WireFormat)
    wireFormat = arg4;
  else
    wireFormat = undefined;

  if (typeof arg3 === 'boolean')
    useSync = arg3;
  else if (typeof arg4 === 'boolean')
    useSync = arg4;
  else if (typeof arg5 === 'boolean')
    useSync = arg5;
  else
    useSync = false;

  var publicKey;
  if (publicKeyOrCertificate instanceof CertificateV2) {
    try {
      publicKey = publicKeyOrCertificate.getPublicKey();
    } catch (ex) {
      return SyncPromise.resolve(false);
    }
  }
  else
    publicKey = publicKeyOrCertificate;

  var encoding = data.wireEncode(wireFormat);
  return VerificationHelpers.verifySignaturePromise
    (encoding.signedBuf(), data.getSignature().getSignature(), publicKey,
     digestAlgorithm, useSync);
};

/**
 * Verify the Interest packet using the public key, where the last two name
 * components are the SignatureInfo and signature bytes. This does not check the
 * type of public key or digest algorithm against the type of SignatureInfo such
 * as Sha256WithRsaSignature.
 * @param {Interest} interest The Interest packet to verify.
 * @param {PublicKey|Buffer|Blob|CertificateV2} publicKeyOrCertificate The
 * object containing the public key, or the public key DER which is used to make
 * the PublicKey object, or the certificate containing the public key.
 * @param {number} digestAlgorithm (optional) The digest algorithm as an int
 * from the DigestAlgorithm enum. If omitted, use DigestAlgorithm.SHA256.
 * @param {WireFormat} wireFormat (optional) A WireFormat object used to encode
 * the Interest packet. If omitted, use WireFormat getDefaultWireFormat().
 * @param {boolean} useSync (optional) If true then return a SyncPromise which
 * is already fulfilled. If omitted or false, this may return a SyncPromise or
 * an async Promise.
 * @return {Promise|SyncPromise} A promise which returns true if verification
 * succeeds, false if verification fails, or a promise rejected with Error for
 * an invalid public key type or digestAlgorithm.
 */
VerificationHelpers.verifyInterestSignaturePromise = function
  (interest, publicKeyOrCertificate, digestAlgorithm, wireFormat, useSync)
{
  var arg3 = digestAlgorithm;
  var arg4 = wireFormat;
  var arg5 = useSync;
  // arg3,            arg4,       arg5
  // digestAlgorithm, wireFormat, useSync
  // digestAlgorithm, wireFormat, null
  // digestAlgorithm, useSync,    null
  // digestAlgorithm, null,       null
  // wireFormat,      useSync,    null
  // wireFormat,      null,       null
  // useSync,         null,       null
  // null,            null,       null
  if (typeof arg3 === 'number')
    digestAlgorithm = arg3;
  else
    digestAlgorithm = undefined;

  if (arg3 instanceof WireFormat)
    wireFormat = arg3;
  else if (arg4 instanceof WireFormat)
    wireFormat = arg4;
  else
    wireFormat = undefined;

  if (typeof arg3 === 'boolean')
    useSync = arg3;
  else if (typeof arg4 === 'boolean')
    useSync = arg4;
  else if (typeof arg5 === 'boolean')
    useSync = arg5;
  else
    useSync = false;

  var publicKey;
  if (publicKeyOrCertificate instanceof CertificateV2) {
    try {
      publicKey = publicKeyOrCertificate.getPublicKey();
    } catch (ex) {
      return SyncPromise.resolve(false);
    }
  }
  else
    publicKey = publicKeyOrCertificate;

  if (wireFormat == undefined)
    wireFormat = WireFormat.getDefaultWireFormat();
  var signature = VerificationHelpers.extractSignature_(interest, wireFormat);
  if (signature == null)
    return SyncPromise.resolve(false);

  var encoding = interest.wireEncode(wireFormat);
  return VerificationHelpers.verifySignaturePromise
    (encoding.signedBuf(), signature.getSignature(), publicKey, digestAlgorithm,
     useSync);
};

/**
 * Verify the buffer against the digest using the digest algorithm.
 * @param {Buffer|Blob} buffer The input buffer to verify.
 * @param {Buffer|Blob} digest The digest bytes.
 * @param {number} digestAlgorithm The digest algorithm as an int from the
 * DigestAlgorithm enum, such as DigestAlgorithm.SHA256.
 * @return {boolean} true if verification succeeds, false if verification fails.
 * @throws Error for an invalid digestAlgorithm.
 */
VerificationHelpers.verifyDigest = function(buffer, digest, digestAlgorithm)
{
  if (buffer instanceof Blob)
    buffer = buffer.buf();
  if (digest instanceof Blob)
    digest = digest.buf();

  if (digestAlgorithm == DigestAlgorithm.SHA256) {
    var hash = Crypto.createHash('sha256');
    hash.update(buffer);
    var computedDigest = hash.digest();

    // Use a loop to compare since it handles different array types.
    if (digest.length != computedDigest.length)
      return false;
    for (var i = 0; i < digest.length; ++i) {
      if (digest[i] != computedDigest[i])
        return false;
    }
    return true;
  }
  else
    throw new Error("verifyDigest: Invalid digest algorithm");
};

/**
 * Extract the signature information from the interest name.
 * @param {Interest} interest The interest whose signature is needed.
 * @param {WireFormat} wireFormat The wire format used to decode signature
 * information from the interest name.
 * @return {Signature} The Signature object, or null if can't decode.
 */
VerificationHelpers.extractSignature_ = function(interest, wireFormat)
{
  if (interest.getName().size() < 2)
    return null;

  try {
    return wireFormat.decodeSignatureInfoAndValue
      (interest.getName().get(-2).getValue().buf(),
       interest.getName().get(-1).getValue().buf(), false);
  } catch (ex) {
    return null;
  }
};

// The first time verify is called, it sets this to determine if a signature
// buffer needs to be converted to a string for the crypto verifier.
VerificationHelpers.verifyUsesString_ = null;
VerificationHelpers.setVerifyUsesString_ = function()
{
  var hashResult = Crypto.createHash('sha256').digest();
  // If the hash result is a string, we assume that this is a version of
  //   crypto where verify also uses a string signature.
  VerificationHelpers.verifyUsesString_ = (typeof hashResult === 'string');
};