// -*- indent-tabs-mode: nil; js-indent-level: 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";

do_get_profile(); // must be called before getting nsIX509CertDB
const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
  Ci.nsIX509CertDB
);

const { QWACs } = ChromeUtils.importESModule(
  "resource://gre/modules/psm/QWACs.sys.mjs"
);

async function verify_1_qwacs(filename, expectSuccess, extraCertNames = []) {
  let cert = constructCertFromFile(filename);
  let result = await certdb.asyncVerifyQWAC(
    Ci.nsIX509CertDB.OneQWAC,
    cert,
    "example.com",
    extraCertNames.map(filename => constructCertFromFile(filename))
  );
  equal(
    result,
    expectSuccess,
    `${filename} ${expectSuccess ? "should" : "should not"} verify as 1-QWAC`
  );
}

add_task(async function test_verify_1_qwacs() {
  Services.prefs.clearUserPref("security.qwacs.enable_test_trust_anchors");
  // By default, the QWACs test trust anchors are not used.
  await verify_1_qwacs("test_qwacs/1-qwac.pem", false);
  await verify_1_qwacs("test_qwacs/1-qwac-qevcpw.pem", false);

  Services.prefs.setBoolPref("security.qwacs.enable_test_trust_anchors", true);

  await verify_1_qwacs("test_qwacs/1-qwac.pem", true);
  await verify_1_qwacs("test_qwacs/1-qwac-qevcpw.pem", true);

  await verify_1_qwacs("test_qwacs/1-qwac-other-optional-qcs.pem", true);

  // One or more intermediates may be necessary for path building.
  await verify_1_qwacs("test_qwacs/1-qwac-via-intermediate.pem", false);
  await verify_1_qwacs("test_qwacs/1-qwac-via-intermediate.pem", true, [
    "test_qwacs/test-int.pem",
  ]);

  await verify_1_qwacs("test_qwacs/empty-qc-type-statement.pem", false);
  await verify_1_qwacs("test_qwacs/missing-qc-type-statement.pem", false);
  await verify_1_qwacs("test_qwacs/missing-qcs-compliance.pem", false);
  await verify_1_qwacs("test_qwacs/wrong-qc-type.pem", false);
  await verify_1_qwacs("test_qwacs/no-1-qwac-policies.pem", false);
  await verify_1_qwacs("test_qwacs/no-policies.pem", false);
  await verify_1_qwacs("test_qwacs/2-qwac.pem", false);
});

async function verify_2_qwacs(
  filename,
  expectSuccess,
  hostname = "example.com"
) {
  let cert = constructCertFromFile(filename);
  let result = await certdb.asyncVerifyQWAC(
    Ci.nsIX509CertDB.TwoQWAC,
    cert,
    hostname,
    []
  );
  equal(
    result,
    expectSuccess,
    `${filename} ${expectSuccess ? "should" : "should not"} verify as 2-QWAC`
  );
}

add_task(async function test_verify_2_qwacs() {
  Services.prefs.clearUserPref("security.qwacs.enable_test_trust_anchors");
  // By default, the QWACs test trust anchors are not used.
  await verify_2_qwacs("test_qwacs/2-qwac.pem", false);

  Services.prefs.setBoolPref("security.qwacs.enable_test_trust_anchors", true);

  await verify_2_qwacs("test_qwacs/2-qwac.pem", true);

  await verify_2_qwacs("test_qwacs/1-qwac.pem", false);
  await verify_2_qwacs("test_qwacs/2-qwac-no-eku.pem", false);
  await verify_2_qwacs("test_qwacs/2-qwac-tls-server-eku.pem", false);
  await verify_2_qwacs("test_qwacs/2-qwac-multiple-key-purpose-eku.pem", false);
  await verify_2_qwacs("test_qwacs/2-qwac.pem", false, "example.org");
});

// Produces base64url(hash(base64url(certificate DER)))
async function certificateHash(certificate, hashAlg) {
  let hash = await crypto.subtle.digest(
    hashAlg.replace("S", "SHA-"),
    new Uint8Array(
      stringToArray(
        QWACs.toBase64URLEncoding(arrayToString(certificate.getRawDER()))
      )
    )
  );
  return QWACs.toBase64URLEncoding(arrayToString(new Uint8Array(hash)));
}

const kTLSCertificateBindingEE = constructCertFromFile("test_qwacs/2-qwac.pem");

const kTLSCertificateBindingHeader = {
  alg: "",
  cty: "TLS-Certificate-Binding-v1",
  // RFC 7515 Section 4.1.6: "Each string in the array is a base64-encoded
  // (Section 4 of [RFC4648] -- not base64url-encoded) DER [ITU.X690.2008] PKIX
  // certificate value."
  x5c: [],
  sigD: {
    mId: "http://uri.etsi.org/19182/ObjectIdByURIHash",
    pars: [],
    hashM: "",
    hashV: [],
  },
};

async function makeBindingHeader(
  certificateChain,
  certificatesToBind,
  signingAlg,
  hashAlg
) {
  let header = structuredClone(kTLSCertificateBindingHeader);
  header.alg = signingAlg;
  header.x5c = certificateChain.map(c => btoa(arrayToString(c.getRawDER())));
  header.sigD.hashM = hashAlg;
  for (let toBind of certificatesToBind) {
    header.sigD.pars.push("");
    header.sigD.hashV.push(await certificateHash(toBind, hashAlg));
  }
  return header;
}

add_task(async function test_validate_tls_certificate_binding_header() {
  let serverCertificate = constructCertFromFile("bad_certs/default-ee.pem");
  let testHeader = await makeBindingHeader(
    [kTLSCertificateBindingEE],
    [serverCertificate],
    "RS256",
    "S256"
  );
  let validatedHeader = QWACs.validateTLSCertificateBindingHeader(testHeader);
  ok(validatedHeader, "header should validate successfully");
  deepEqual(validatedHeader.algorithm, {
    name: "RSASSA-PKCS1-v1_5",
    hash: "SHA-256",
  });
  equal(validatedHeader.certificates.length, 1);
  equal(validatedHeader.hashAlg, "SHA-256");
  equal(validatedHeader.hashes.length, 1);

  let headerWithExtraKey = structuredClone(testHeader);
  headerWithExtraKey.extra = "foo";
  ok(
    !QWACs.validateTLSCertificateBindingHeader(headerWithExtraKey),
    "header with extra key should not validate"
  );

  let headerWithExtraSigDKey = structuredClone(testHeader);
  headerWithExtraSigDKey.sigD.additional = "bar";
  ok(
    !QWACs.validateTLSCertificateBindingHeader(headerWithExtraSigDKey),
    "header with extra key in sigD should not parse"
  );

  let headerWithWrongCty = structuredClone(testHeader);
  headerWithWrongCty.cty = "TLS-Certificate-Binding-v2";
  ok(
    !QWACs.validateTLSCertificateBindingHeader(headerWithWrongCty),
    "header with wrong cty should not parse"
  );

  let headerWithWrongMId = structuredClone(testHeader);
  headerWithWrongMId.sigD.mId = "http://example.org";
  ok(
    !QWACs.validateTLSCertificateBindingHeader(headerWithWrongMId),
    "header with wrong sigD.mId should not parse"
  );

  let headerWithTooManyPars = structuredClone(testHeader);
  headerWithTooManyPars.sigD.pars.push("");
  ok(
    !QWACs.validateTLSCertificateBindingHeader(headerWithTooManyPars),
    "header with too many sigD.pars elements should not parse"
  );

  let headerWithInvalidHashV = structuredClone(testHeader);
  headerWithInvalidHashV.sigD.hashV[0] = 1234;
  ok(
    !QWACs.validateTLSCertificateBindingHeader(headerWithInvalidHashV),
    "header with invalid sigD.hashV should not parse"
  );

  let headerWithTooManyHashV = structuredClone(testHeader);
  headerWithTooManyHashV.sigD.hashV.push(headerWithTooManyHashV.sigD.hashV[0]);
  ok(
    !QWACs.validateTLSCertificateBindingHeader(headerWithTooManyHashV),
    "header with too many sigD.hashV elements should not parse"
  );

  let headerWithEmptyX5c = structuredClone(testHeader);
  headerWithEmptyX5c.x5c = [];
  ok(
    !QWACs.validateTLSCertificateBindingHeader(headerWithEmptyX5c),
    "header with empty x5c should not parse"
  );

  let headerWithUnsupportedAlg = structuredClone(testHeader);
  headerWithUnsupportedAlg.alg = "RS384";
  ok(
    !QWACs.validateTLSCertificateBindingHeader(headerWithUnsupportedAlg),
    "header with unsupported alg should not parse"
  );

  let headerWithUnsupportedHashM = structuredClone(testHeader);
  headerWithUnsupportedHashM.sigD.hashM = "S224";
  ok(
    !QWACs.validateTLSCertificateBindingHeader(headerWithUnsupportedHashM),
    "header with unsupported sigD.hashM should not parse"
  );

  let headerWithOptionalHeaders = structuredClone(testHeader);
  headerWithOptionalHeaders.kid = "optional kid";
  headerWithOptionalHeaders.iat = "optional iat";
  ok(
    QWACs.validateTLSCertificateBindingHeader(headerWithOptionalHeaders),
    "header with optional headers should parse"
  );

  let headerWithX5tS256Match = structuredClone(testHeader);
  let signingCertificateHash = await crypto.subtle.digest(
    "SHA-256",
    new Uint8Array(kTLSCertificateBindingEE.getRawDER())
  );
  headerWithX5tS256Match["x5t#S256"] = QWACs.toBase64URLEncoding(
    arrayToString(new Uint8Array(signingCertificateHash))
  );
  ok(
    QWACs.validateTLSCertificateBindingHeader(headerWithX5tS256Match),
    "header with matching x5t#S256 should parse"
  );

  let headerWithX5tS256Mismatch = structuredClone(testHeader);
  headerWithX5tS256Mismatch["x5t#S256"] =
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
  ok(
    !QWACs.validateTLSCertificateBindingHeader(headerWithX5tS256Mismatch),
    "header with x5t#S256 mismatch should not parse"
  );

  let headerWithBadExp = structuredClone(testHeader);
  headerWithBadExp.exp = "not a number";
  ok(
    !QWACs.validateTLSCertificateBindingHeader(headerWithBadExp),
    "header with bad expiration time should not parse"
  );

  let expiredHeader = structuredClone(testHeader);
  expiredHeader.exp = "946684800";
  ok(
    !QWACs.validateTLSCertificateBindingHeader(expiredHeader),
    "expired header should not parse"
  );

  let unexpiredHeader = structuredClone(testHeader);
  unexpiredHeader.exp = "4102444800";
  ok(
    QWACs.validateTLSCertificateBindingHeader(unexpiredHeader),
    "unexpired header should parse"
  );
});

async function validate_tls_certificate_binding_header_with_algorithms(
  signatureAlg,
  hashAlg,
  expectedSignatureAlg,
  expectedHashAlg
) {
  let serverCertificate = constructCertFromFile("bad_certs/default-ee.pem");
  let testHeader = await makeBindingHeader(
    [kTLSCertificateBindingEE],
    [serverCertificate],
    signatureAlg,
    hashAlg
  );
  let validatedHeader = QWACs.validateTLSCertificateBindingHeader(testHeader);
  ok(validatedHeader, "header should validate successfully");
  deepEqual(validatedHeader.algorithm, expectedSignatureAlg);
  equal(validatedHeader.certificates.length, 1);
  equal(validatedHeader.hashAlg, expectedHashAlg);
  equal(validatedHeader.hashes.length, 1);
}

add_task(
  async function test_validate_tls_certificate_binding_header_with_algorithms() {
    let options = [
      {
        signatureAlg: "RS256",
        hashAlg: "S256",
        expectedSignatureAlg: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
        expectedHashAlg: "SHA-256",
      },
      {
        signatureAlg: "PS256",
        hashAlg: "S384",
        expectedSignatureAlg: {
          name: "RSA-PSS",
          saltLength: 32,
          hash: "SHA-256",
        },
        expectedHashAlg: "SHA-384",
      },
      {
        signatureAlg: "ES256",
        hashAlg: "S512",
        expectedSignatureAlg: {
          name: "ECDSA",
          namedCurve: "P-256",
          hash: "SHA-256",
        },
        expectedHashAlg: "SHA-512",
      },
    ];
    for (let option of options) {
      await validate_tls_certificate_binding_header_with_algorithms(
        option.signatureAlg,
        option.hashAlg,
        option.expectedSignatureAlg,
        option.expectedHashAlg
      );
    }
  }
);

async function sign(privateKeyInfo, algorithm, header) {
  let toSign = new Uint8Array(stringToArray(header + "."));
  let key = await crypto.subtle.importKey(
    "pkcs8",
    privateKeyInfo,
    algorithm,
    true,
    ["sign"]
  );
  let signature = await crypto.subtle.sign(algorithm, key, toSign);
  return (
    header +
    ".." +
    QWACs.toBase64URLEncoding(arrayToString(new Uint8Array(signature)))
  );
}

async function signTLSCertificateBinding(header, key, algorithm) {
  return sign(
    key,
    algorithm,
    QWACs.toBase64URLEncoding(JSON.stringify(header))
  );
}

async function verify_tls_certificate_binding_signature(
  headerSignatureAlgorithm,
  headerHashAlgorithm,
  signatureAlgorithm,
  signingKeyFilename,
  bindingCertificateFilename,
  serverCertificateFilename,
  presentedServerCertificateFilename,
  expectSuccess,
  expectedCertificateSubject
) {
  let signingKey = new Uint8Array(
    stringToArray(
      QWACs.fromBase64URLEncoding(
        pemToBase64(readFile(do_get_file(signingKeyFilename, false)))
      )
    )
  );
  let bindingCertificate = constructCertFromFile(bindingCertificateFilename);
  let serverCertificate = constructCertFromFile(serverCertificateFilename);
  let testHeader = await makeBindingHeader(
    [bindingCertificate],
    [serverCertificate],
    headerSignatureAlgorithm,
    headerHashAlgorithm
  );
  let binding = await signTLSCertificateBinding(
    testHeader,
    signingKey,
    signatureAlgorithm
  );
  let presentedServerCertificate = constructCertFromFile(
    presentedServerCertificateFilename
  );
  let qwac = await QWACs.verifyTLSCertificateBinding(
    binding,
    presentedServerCertificate,
    "example.com"
  );
  equal(
    !!qwac,
    expectSuccess,
    `TLS certificate binding ${expectSuccess ? "should" : "should not"} verify correctly`
  );
  if (expectSuccess) {
    equal(
      qwac.commonName,
      expectedCertificateSubject,
      "Verification should return the expected certificate"
    );
  }
}

add_task(async function test_verify_tls_certificate_binding_signatures() {
  let options = [
    {
      headerSignatureAlgorithm: "RS256",
      headerHashAlgorithm: "S512",
      signatureAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
      signingKeyFilename: "bad_certs/default-ee.key",
      bindingCertificateFilename: "test_qwacs/2-qwac.pem",
      serverCertificateFilename: "bad_certs/default-ee.pem",
      presentedServerCertificateFilename: "bad_certs/default-ee.pem",
      expectSuccess: true,
      expectedCertificateSubject: "2-QWAC",
    },
    // presented server certificate / bound server certificate mismatch
    {
      headerSignatureAlgorithm: "RS256",
      headerHashAlgorithm: "S512",
      signatureAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
      signingKeyFilename: "bad_certs/default-ee.key",
      bindingCertificateFilename: "test_qwacs/2-qwac.pem",
      serverCertificateFilename: "bad_certs/default-ee.pem",
      presentedServerCertificateFilename:
        "bad_certs/ee-from-missing-intermediate.pem",
      expectSuccess: false,
      expectedCertificateSubject: null,
    },
    {
      headerSignatureAlgorithm: "PS256",
      headerHashAlgorithm: "S256",
      signatureAlgorithm: {
        name: "RSA-PSS",
        hash: "SHA-256",
        saltLength: 32,
      },
      signingKeyFilename: "bad_certs/default-ee.key",
      bindingCertificateFilename: "test_qwacs/2-qwac.pem",
      serverCertificateFilename: "bad_certs/default-ee.pem",
      presentedServerCertificateFilename: "bad_certs/default-ee.pem",
      expectSuccess: true,
      expectedCertificateSubject: "2-QWAC",
    },
    {
      headerSignatureAlgorithm: "ES256",
      headerHashAlgorithm: "S384",
      signatureAlgorithm: {
        name: "ECDSA",
        namedCurve: "P-256",
        hash: "SHA-256",
      },
      signingKeyFilename: "test_qwacs/secp256r1.key",
      bindingCertificateFilename: "test_qwacs/2-qwac-ec.pem",
      serverCertificateFilename: "bad_certs/default-ee.pem",
      presentedServerCertificateFilename: "bad_certs/default-ee.pem",
      expectSuccess: true,
      expectedCertificateSubject: "2-QWAC with EC key",
    },
    // header signature algorithm / actual signature algorithm mismatch
    {
      headerSignatureAlgorithm: "RS256",
      headerHashAlgorithm: "S384",
      signatureAlgorithm: {
        name: "ECDSA",
        namedCurve: "P-256",
        hash: "SHA-256",
      },
      signingKeyFilename: "test_qwacs/secp256r1.key",
      bindingCertificateFilename: "test_qwacs/2-qwac-ec.pem",
      serverCertificateFilename: "bad_certs/default-ee.pem",
      presentedServerCertificateFilename: "bad_certs/default-ee.pem",
      expectSuccess: false,
      expectedCertificateSubject: null,
    },
  ];

  for (let option of options) {
    await verify_tls_certificate_binding_signature(
      option.headerSignatureAlgorithm,
      option.headerHashAlgorithm,
      option.signatureAlgorithm,
      option.signingKeyFilename,
      option.bindingCertificateFilename,
      option.serverCertificateFilename,
      option.presentedServerCertificateFilename,
      option.expectSuccess,
      option.expectedCertificateSubject
    );
  }
});
