// -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

"use strict";

// This test tests that the nsIX509Cert.dbKey and nsIX509CertDB.findCertByDBKey
// APIs work as expected. That is, getting a certificate's dbKey and using it
// in findCertByDBKey should return the same certificate. Also, for backwards
// compatibility, findCertByDBKey should ignore any whitespace in its input
// (even though now nsIX509Cert.dbKey will never have whitespace in it).

function hexStringToBytes(hex) {
  let bytes = [];
  for (let hexByteStr of hex.split(":")) {
    bytes.push(parseInt(hexByteStr, 16));
  }
  return bytes;
}

function encodeCommonNameAsBytes(commonName) {
  // The encoding will look something like this (in hex):
  // 30 (SEQUENCE) <length of contents>
  //    31 (SET) <length of contents>
  //       30 (SEQUENCE) <length of contents>
  //          06 (OID) 03 (length)
  //             55 04 03 (id-at-commonName)
  //          0C (UTF8String) <length of common name>
  //             <common name bytes>
  // To make things simple, it would be nice to have the length of each
  // component be less than 128 bytes (so we can have single-byte lengths).
  // For this to hold, the maximum length of the contents of the outermost
  // SEQUENCE must be 127. Everything not in the contents of the common name
  // will take up 11 bytes, so the value of the common name itself can be at
  // most 116 bytes.
  Assert.lessOrEqual(
    commonName.length,
    116,
    "test assumption: common name can't be longer than 116 bytes (makes " +
      "DER encoding easier)"
  );
  let commonNameOIDBytes = [0x06, 0x03, 0x55, 0x04, 0x03];
  let commonNameBytes = [0x0c, commonName.length];
  for (let i = 0; i < commonName.length; i++) {
    commonNameBytes.push(commonName.charCodeAt(i));
  }
  let bytes = commonNameOIDBytes.concat(commonNameBytes);
  bytes.unshift(bytes.length);
  bytes.unshift(0x30); // SEQUENCE
  bytes.unshift(bytes.length);
  bytes.unshift(0x31); // SET
  bytes.unshift(bytes.length);
  bytes.unshift(0x30); // SEQUENCE
  return bytes;
}

function testInvalidDBKey(certDB, dbKey) {
  throws(
    () => certDB.findCertByDBKey(dbKey),
    /NS_ERROR_ILLEGAL_INPUT/,
    `findCertByDBKey(${dbKey}) should raise NS_ERROR_ILLEGAL_INPUT`
  );
}

function testDBKeyForNonexistentCert(certDB, dbKey) {
  let cert = certDB.findCertByDBKey(dbKey);
  ok(!cert, "shouldn't find cert for given dbKey");
}

function byteArrayToByteString(bytes) {
  let byteString = "";
  for (let b of bytes) {
    byteString += String.fromCharCode(b);
  }
  return byteString;
}

function run_test() {
  do_get_profile();
  let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
    Ci.nsIX509CertDB
  );
  let cert = constructCertFromFile("bad_certs/test-ca.pem");
  equal(
    cert.issuerName,
    "CN=" + cert.issuerCommonName,
    "test assumption: this certificate's issuer distinguished name " +
      "consists only of a common name"
  );
  let issuerBytes = encodeCommonNameAsBytes(cert.issuerCommonName);
  Assert.less(
    issuerBytes.length,
    256,
    "test assumption: length of encoded issuer is less than 256 bytes"
  );
  let serialNumberBytes = hexStringToBytes(cert.serialNumber);
  Assert.less(
    serialNumberBytes.length,
    256,
    "test assumption: length of encoded serial number is less than 256 bytes"
  );
  let dbKeyHeader = [
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    serialNumberBytes.length,
    0,
    0,
    0,
    issuerBytes.length,
  ];
  let expectedDbKeyBytes = dbKeyHeader.concat(serialNumberBytes, issuerBytes);
  let expectedDbKey = btoa(byteArrayToByteString(expectedDbKeyBytes));
  equal(
    cert.dbKey,
    expectedDbKey,
    "actual and expected dbKey values should match"
  );

  let certFromDbKey = certDB.findCertByDBKey(expectedDbKey);
  ok(
    areCertsEqual(certFromDbKey, cert),
    "nsIX509CertDB.findCertByDBKey should find the right certificate"
  );

  Assert.greater(
    expectedDbKey.length,
    64,
    "test assumption: dbKey should be longer than 64 characters"
  );
  let expectedDbKeyWithCRLF = expectedDbKey.replace(/(.{64})/, "$1\r\n");
  Assert.equal(
    expectedDbKeyWithCRLF.indexOf("\r\n"),
    64,
    "test self-check: adding CRLF to dbKey should succeed"
  );
  certFromDbKey = certDB.findCertByDBKey(expectedDbKeyWithCRLF);
  ok(
    areCertsEqual(certFromDbKey, cert),
    "nsIX509CertDB.findCertByDBKey should work with dbKey with CRLF"
  );

  let expectedDbKeyWithSpaces = expectedDbKey.replace(/(.{64})/, "$1  ");
  Assert.equal(
    expectedDbKeyWithSpaces.indexOf("  "),
    64,
    "test self-check: adding spaces to dbKey should succeed"
  );
  certFromDbKey = certDB.findCertByDBKey(expectedDbKeyWithSpaces);
  ok(
    areCertsEqual(certFromDbKey, cert),
    "nsIX509CertDB.findCertByDBKey should work with dbKey with spaces"
  );

  // Test some invalid dbKey values.
  testInvalidDBKey(certDB, "AAAA"); // Not long enough.
  // No header.
  testInvalidDBKey(
    certDB,
    btoa(
      byteArrayToByteString(
        [0, 0, 0, serialNumberBytes.length, 0, 0, 0, issuerBytes.length].concat(
          serialNumberBytes,
          issuerBytes
        )
      )
    )
  );
  testInvalidDBKey(
    certDB,
    btoa(
      byteArrayToByteString([
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        255,
        255,
        255,
        255, // serial number length is way too long
        255,
        255,
        255,
        255, // issuer length is way too long
        0,
        0,
        0,
        0,
      ])
    )
  );
  // Truncated issuer.
  testInvalidDBKey(
    certDB,
    btoa(
      byteArrayToByteString([
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 10, 1, 1, 2, 3,
      ])
    )
  );
  // Issuer doesn't decode to valid common name.
  testDBKeyForNonexistentCert(
    certDB,
    btoa(
      byteArrayToByteString([
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 1, 1, 2, 3,
      ])
    )
  );

  // zero-length serial number and issuer -> no such certificate
  testDBKeyForNonexistentCert(
    certDB,
    btoa(
      byteArrayToByteString([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
    )
  );
}
