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