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