# Checks that `security.pki` options are working in curl and the main browser
# engines: Gecko (via Firefox), Chromium, QtWebEngine (via qutebrowser).
# The test checks that certificates issued by a custom
# trusted CA are accepted but those from an unknown CA are rejected.

{ runTest, pkgs }:

let
  inherit (pkgs) lib;

  makeCert =
    { caName, domain }:
    pkgs.runCommand "example-cert" { buildInputs = [ pkgs.gnutls ]; } ''
      mkdir $out

      # CA cert template
      cat >ca.template <<EOF
      organization = "${caName}"
      cn = "${caName}"
      expiration_days = 365
      ca
      cert_signing_key
      crl_signing_key
      EOF

      # server cert template
      cat >server.template <<EOF
      organization = "An example company"
      cn = "${domain}"
      expiration_days = 30
      dns_name = "${domain}"
      encryption_key
      signing_key
      EOF

      # generate CA keypair
      certtool                \
        --generate-privkey    \
        --key-type rsa        \
        --sec-param High      \
        --outfile $out/ca.key
      certtool                     \
        --generate-self-signed     \
        --load-privkey $out/ca.key \
        --template ca.template     \
        --outfile $out/ca.crt

      # generate server keypair
      certtool                    \
        --generate-privkey        \
        --key-type rsa            \
        --sec-param High          \
        --outfile $out/server.key
      certtool                            \
        --generate-certificate            \
        --load-privkey $out/server.key    \
        --load-ca-privkey $out/ca.key     \
        --load-ca-certificate $out/ca.crt \
        --template server.template        \
        --outfile $out/server.crt
    '';

  example-good-cert = makeCert {
    caName = "Example good CA";
    domain = "good.example.com";
  };

  example-bad-cert = makeCert {
    caName = "Unknown CA";
    domain = "bad.example.com";
  };

  webserverConfig = {
    networking.hosts."127.0.0.1" = [
      "good.example.com"
      "bad.example.com"
    ];
    security.pki.certificateFiles = [ "${example-good-cert}/ca.crt" ];

    services.nginx.enable = true;
    services.nginx.virtualHosts."good.example.com" = {
      onlySSL = true;
      sslCertificate = "${example-good-cert}/server.crt";
      sslCertificateKey = "${example-good-cert}/server.key";
      locations."/".extraConfig = ''
        add_header Content-Type text/plain;
        return 200 'It works!';
      '';
    };
    services.nginx.virtualHosts."bad.example.com" = {
      onlySSL = true;
      sslCertificate = "${example-bad-cert}/server.crt";
      sslCertificateKey = "${example-bad-cert}/server.key";
      locations."/".extraConfig = ''
        add_header Content-Type text/plain;
        return 200 'It does not work!';
      '';
    };
  };

  curlTest = runTest {
    name = "custom-ca-curl";
    meta.maintainers = with lib.maintainers; [ rnhmjoj ];
    nodes.machine = { ... }: webserverConfig;
    testScript = ''
      with subtest("Good certificate is trusted in curl"):
          machine.wait_for_unit("nginx")
          machine.wait_for_open_port(443)
          machine.succeed("curl -fv https://good.example.com")

      with subtest("Unknown CA is untrusted in curl"):
          machine.fail("curl -fv https://bad.example.com")
    '';
  };

  mkBrowserTest =
    browser: testParams:
    runTest {
      name = "custom-ca-${browser}";
      meta.maintainers = with lib.maintainers; [ rnhmjoj ];

      enableOCR = true;

      nodes.machine =
        { pkgs, ... }:
        {
          imports = [
            ./common/user-account.nix
            ./common/x11.nix
            webserverConfig
          ];

          # chromium-based browsers refuse to run as root
          test-support.displayManager.auto.user = "alice";

          # machine often runs out of memory with less
          virtualisation.memorySize = 1024;

          environment.systemPackages = [
            pkgs.xdotool
            pkgs.${browser}
          ];
        };

      testScript = ''
        from typing import Tuple
        def execute_as(user: str, cmd: str) -> Tuple[int, str]:
            """
            Run a shell command as a specific user.
            """
            return machine.execute(f"sudo -u {user} {cmd}")


        def wait_for_window_as(user: str, cls: str) -> None:
            """
            Wait until a X11 window of a given user appears.
            """

            def window_is_visible(last_try: bool) -> bool:
                ret, stdout = execute_as(user, f"xdotool search --onlyvisible --class {cls}")
                if last_try:
                    machine.log(f"Last chance to match {cls} on the window list")
                return ret == 0

            with machine.nested("Waiting for a window to appear"):
                retry(window_is_visible)


        machine.start()
        machine.wait_for_x()

        command = "${browser} ${testParams.args or ""}"
        with subtest("Good certificate is trusted in ${browser}"):
            execute_as(
                "alice", f"{command} https://good.example.com >&2 &"
            )
            wait_for_window_as("alice", "${browser}")
            machine.sleep(4)
            execute_as("alice", "xdotool key ctrl+r")  # reload to be safe
            machine.wait_for_text("It works!")
            machine.screenshot("good${browser}")
            execute_as("alice", "xdotool key ctrl+w")  # close tab

        with subtest("Unknown CA is untrusted in ${browser}"):
            execute_as("alice", f"{command} https://bad.example.com >&2 &")
            machine.wait_for_text("${testParams.error}")
            machine.screenshot("bad${browser}")
      '';
    };

in

{
  curl = curlTest;
}
// lib.mapAttrs mkBrowserTest {
  firefox = {
    error = "Security Risk";
  };
  chromium = {
    error = "not private";
  };
  qutebrowser = {
    args = "-T";
    error = "Certificate error";
  };
}
