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

import ./make-test-python.nix ({ pkgs, lib, ... }:

let
  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";
    };

in

{
  name = "custom-ca";
  meta.maintainers = with lib.maintainers; [ rnhmjoj ];

  enableOCR = true;

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

      # chromium-based browsers refuse to run as root
      test-support.displayManager.auto.user = "alice";
      # browsers may hang with the default memory
      virtualisation.memorySize = "500";

      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!';
          '';
        };

      environment.systemPackages = with pkgs;
        [ xdotool firefox chromium falkon midori ];
    };

  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()

    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")

    browsers = ["firefox", "chromium", "falkon", "midori"]
    errors = ["Security Risk", "not private", "Certificate Error", "Security"]

    machine.wait_for_x()
    for browser, error in zip(browsers, errors):
        with subtest("Good certificate is trusted in " + browser):
            execute_as(
                "alice", f"env P11_KIT_DEBUG=trust {browser} https://good.example.com & >&2"
            )
            wait_for_window_as("alice", browser)
            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"{browser} https://bad.example.com & >&2")
            machine.wait_for_text(error)
            machine.screenshot("bad" + browser)
            machine.succeed("pkill " + browser)
  '';
})
