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