/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {PublicKeyVerify} from '../signature/internal/public_key_verify';

import * as Bytes from './bytes';
import * as ecdsaSign from './ecdsa_sign';
import * as ecdsaVerify from './ecdsa_verify';
import * as EllipticCurves from './elliptic_curves';
import * as Random from './random';
import * as Validators from './validators';
import {WYCHEPROOF_ECDSA_TEST_VECTORS} from './wycheproof_ecdsa_test_vectors';

describe('ecdsa verify test', function() {
  beforeEach(function() {
    // Use a generous promise timeout for running continuously.
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
  });

  afterEach(function() {
    // Reset the promise timeout to default value.
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
  });

  it('verify', async function() {
    const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
    const signer = await ecdsaSign.fromJsonWebKey(
        await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256');
    const verifier = await ecdsaVerify.fromJsonWebKey(
        await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256');
    for (let i = 0; i < 100; i++) {
      const data = Random.randBytes(i);
      const signature = await signer.sign(data);
      expect(await verifier.verify(signature, data)).toBe(true);
    }
  });

  it('verify with der encoding', async function() {
    const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
    const signer = await ecdsaSign.fromJsonWebKey(
        await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256',
        EllipticCurves.EcdsaSignatureEncodingType.DER);
    const verifier = await ecdsaVerify.fromJsonWebKey(
        await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256');
    const verifierDer = await ecdsaVerify.fromJsonWebKey(
        await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256',
        EllipticCurves.EcdsaSignatureEncodingType.DER);
    for (let i = 0; i < 100; i++) {
      const data = Random.randBytes(i);
      const signature = await signer.sign(data);
      expect(await verifier.verify(signature, data)).toBe(false);
      expect(await verifierDer.verify(signature, data)).toBe(true);
    }
  });

  it('constructor with invalid hash', async function() {
    try {
      const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
      await ecdsaVerify.fromJsonWebKey(
          await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-1');
      fail('Should throw an exception.');
      // Preserving old behavior when moving to
      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
      // tslint:disable-next-line:no-any
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'SecurityException: expected SHA-256 (because curve is P-256) but got SHA-1');
    }

    try {
      const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-384');
      await ecdsaVerify.fromJsonWebKey(
          await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256');
      fail('Should throw an exception.');
      // Preserving old behavior when moving to
      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
      // tslint:disable-next-line:no-any
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'SecurityException: expected SHA-384 or SHA-512 (because curve is P-384) but got SHA-256');
    }

    try {
      const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-521');
      await ecdsaVerify.fromJsonWebKey(
          await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256');
      fail('Should throw an exception.');
      // Preserving old behavior when moving to
      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
      // tslint:disable-next-line:no-any
    } catch (e: any) {
      expect(e.toString())
          .toBe(
              'SecurityException: expected SHA-512 (because curve is P-521) but got SHA-256');
    }
  });

  it('constructor with invalid curve', async function() {
    try {
      const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
      const jwk = await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
      jwk.crv = 'blah';
      await ecdsaVerify.fromJsonWebKey(jwk, 'SHA-256');
      fail('Should throw an exception.');
      // Preserving old behavior when moving to
      // https://www.typescriptlang.org/tsconfig#useUnknownInCatchVariables
      // tslint:disable-next-line:no-any
    } catch (e: any) {
      expect(e.toString()).toBe('SecurityException: unsupported curve: blah');
    }
  });

  it('verify modified signature', async function() {
    const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
    const signer = await ecdsaSign.fromJsonWebKey(
        await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256');
    const verifier = await ecdsaVerify.fromJsonWebKey(
        await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256');
    const data = Random.randBytes(20);
    const signature = await signer.sign(data);

    for (let i = 0; i < signature.length; i++) {
      for (let j = 0; j < 8; j++) {
        const s1 = new Uint8Array(signature);
        s1[i] = (s1[i] ^ (1 << j));
        expect(await verifier.verify(s1, data)).toBe(false);
      }
    }
  });

  it('verify modified data', async function() {
    const keyPair = await EllipticCurves.generateKeyPair('ECDSA', 'P-256');
    const signer = await ecdsaSign.fromJsonWebKey(
        await EllipticCurves.exportCryptoKey(keyPair.privateKey!), 'SHA-256');
    const verifier = await ecdsaVerify.fromJsonWebKey(
        await EllipticCurves.exportCryptoKey(keyPair.publicKey!), 'SHA-256');
    const data = Random.randBytes(20);
    const signature = await signer.sign(data);

    for (let i = 0; i < data.length; i++) {
      for (let j = 0; j < 8; j++) {
        const data1 = new Uint8Array(data);
        data1[i] = (data1[i] ^ (1 << j));
        expect(await verifier.verify(signature, data1)).toBe(false);
      }
    }
  });

  it('wycheproof', async function() {
    for (const testGroup of WYCHEPROOF_ECDSA_TEST_VECTORS['testGroups']) {
      try {
        Validators.validateEcdsaParams(
            testGroup['jwk']['crv'], testGroup['sha']);
      } catch (e) {
        // Tink does not support this config.
        continue;
      }
      const verifier =
          await ecdsaVerify.fromJsonWebKey(testGroup['jwk'], testGroup['sha']);
      let errors = '';
      for (const test of testGroup['tests']) {
        errors += await runWycheproofTest(verifier, test);
      }
      if (errors !== '') {
        fail(errors);
      }
    }
  });
});

/**
 * Runs the test with test vector given as an input and returns either empty
 * string or a text describing the failure.
 */
async function runWycheproofTest(
    verifier: PublicKeyVerify,
    test: {'tcId': number, 'msg': string, 'sig': string, 'result': string}):
    Promise<string> {
  try {
    const sig = Bytes.fromHex(test['sig']);
    const msg = Bytes.fromHex(test['msg']);
    const isValid = await verifier.verify(sig, msg);
    if (isValid) {
      if (test['result'] === 'invalid') {
        return 'invalid signature accepted on test ' + test['tcId'] + '\n';
      }
    } else {
      if (test['result'] === 'valid') {
        return 'valid signature rejected on test ' + test['tcId'] + '\n';
      }
    }
  } catch (e) {
    if (test['result'] === 'valid') {
      return 'valid signature rejected on test ' + test['tcId'] +
          ': unexpected exception "' + String(e) + '".\n';
    }
  }
  // If the test passes return an empty string.
  return '';
}
