1# Checks that `security.pki` options are working in curl and the main browser
2# engines: Gecko (via Firefox), Chromium, QtWebEngine (Falkon) and WebKitGTK
3# (via Midori). The test checks that certificates issued by a custom trusted
4# CA are accepted but those from an unknown CA are rejected.
5
6import ./make-test-python.nix ({ pkgs, lib, ... }:
7
8let
9 makeCert = { caName, domain }: pkgs.runCommand "example-cert"
10 { buildInputs = [ pkgs.gnutls ]; }
11 ''
12 mkdir $out
13
14 # CA cert template
15 cat >ca.template <<EOF
16 organization = "${caName}"
17 cn = "${caName}"
18 expiration_days = 365
19 ca
20 cert_signing_key
21 crl_signing_key
22 EOF
23
24 # server cert template
25 cat >server.template <<EOF
26 organization = "An example company"
27 cn = "${domain}"
28 expiration_days = 30
29 dns_name = "${domain}"
30 encryption_key
31 signing_key
32 EOF
33
34 # generate CA keypair
35 certtool \
36 --generate-privkey \
37 --key-type rsa \
38 --sec-param High \
39 --outfile $out/ca.key
40 certtool \
41 --generate-self-signed \
42 --load-privkey $out/ca.key \
43 --template ca.template \
44 --outfile $out/ca.crt
45
46 # generate server keypair
47 certtool \
48 --generate-privkey \
49 --key-type rsa \
50 --sec-param High \
51 --outfile $out/server.key
52 certtool \
53 --generate-certificate \
54 --load-privkey $out/server.key \
55 --load-ca-privkey $out/ca.key \
56 --load-ca-certificate $out/ca.crt \
57 --template server.template \
58 --outfile $out/server.crt
59 '';
60
61 example-good-cert = makeCert
62 { caName = "Example good CA";
63 domain = "good.example.com";
64 };
65
66 example-bad-cert = makeCert
67 { caName = "Unknown CA";
68 domain = "bad.example.com";
69 };
70
71in
72
73{
74 name = "custom-ca";
75 meta.maintainers = with lib.maintainers; [ rnhmjoj ];
76
77 enableOCR = true;
78
79 machine = { pkgs, ... }:
80 { imports = [ ./common/user-account.nix ./common/x11.nix ];
81
82 # chromium-based browsers refuse to run as root
83 test-support.displayManager.auto.user = "alice";
84
85 networking.hosts."127.0.0.1" = [ "good.example.com" "bad.example.com" ];
86 security.pki.certificateFiles = [ "${example-good-cert}/ca.crt" ];
87
88 services.nginx.enable = true;
89 services.nginx.virtualHosts."good.example.com" =
90 { onlySSL = true;
91 sslCertificate = "${example-good-cert}/server.crt";
92 sslCertificateKey = "${example-good-cert}/server.key";
93 locations."/".extraConfig = ''
94 add_header Content-Type text/plain;
95 return 200 'It works!';
96 '';
97 };
98 services.nginx.virtualHosts."bad.example.com" =
99 { onlySSL = true;
100 sslCertificate = "${example-bad-cert}/server.crt";
101 sslCertificateKey = "${example-bad-cert}/server.key";
102 locations."/".extraConfig = ''
103 add_header Content-Type text/plain;
104 return 200 'It does not work!';
105 '';
106 };
107
108 environment.systemPackages = with pkgs; [
109 xdotool
110 firefox
111 chromium
112 qutebrowser
113 midori
114 ];
115 };
116
117 testScript = ''
118 from typing import Tuple
119 def execute_as(user: str, cmd: str) -> Tuple[int, str]:
120 """
121 Run a shell command as a specific user.
122 """
123 return machine.execute(f"sudo -u {user} {cmd}")
124
125
126 def wait_for_window_as(user: str, cls: str) -> None:
127 """
128 Wait until a X11 window of a given user appears.
129 """
130
131 def window_is_visible(last_try: bool) -> bool:
132 ret, stdout = execute_as(user, f"xdotool search --onlyvisible --class {cls}")
133 if last_try:
134 machine.log(f"Last chance to match {cls} on the window list")
135 return ret == 0
136
137 with machine.nested("Waiting for a window to appear"):
138 retry(window_is_visible)
139
140
141 machine.start()
142
143 with subtest("Good certificate is trusted in curl"):
144 machine.wait_for_unit("nginx")
145 machine.wait_for_open_port(443)
146 machine.succeed("curl -fv https://good.example.com")
147
148 with subtest("Unknown CA is untrusted in curl"):
149 machine.fail("curl -fv https://bad.example.com")
150
151 browsers = {
152 "firefox": "Security Risk",
153 "chromium": "not private",
154 "qutebrowser -T": "Certificate error",
155 "midori": "Security"
156 }
157
158 machine.wait_for_x()
159 for command, error in browsers.items():
160 browser = command.split()[0]
161 with subtest("Good certificate is trusted in " + browser):
162 execute_as(
163 "alice", f"env P11_KIT_DEBUG=trust {command} https://good.example.com & >&2"
164 )
165 wait_for_window_as("alice", browser)
166 machine.wait_for_text("It works!")
167 machine.screenshot("good" + browser)
168 execute_as("alice", "xdotool key ctrl+w") # close tab
169
170 with subtest("Unknown CA is untrusted in " + browser):
171 execute_as("alice", f"{command} https://bad.example.com & >&2")
172 machine.wait_for_text(error)
173 machine.screenshot("bad" + browser)
174 machine.succeed("pkill " + browser)
175 '';
176})