1/*
2 Test suite for curl-impersonate
3
4 Abstract:
5 Uses the test suite from the curl-impersonate source repo which:
6
7 1. Performs requests with libcurl and captures the TLS client-hello
8 packets with tcpdump to compare against known-good signatures
9 2. Spins up an nghttpd2 server to test client HTTP/2 headers against
10 known-good headers
11
12 See https://github.com/lwthiker/curl-impersonate/tree/main/tests/signatures
13 for details.
14
15 Notes:
16 - We need to have our own web server running because the tests expect to be able
17 to hit domains like wikipedia.org and the sandbox has no internet
18 - We need to be able to do (verifying) TLS handshakes without internet access.
19 We do that by creating a trusted CA and issuing a cert that includes
20 all of the test domains as subject-alternative names and then spoofs the
21 hostnames in /etc/hosts.
22*/
23
24import ./make-test-python.nix (
25 { pkgs, lib, ... }:
26 let
27 # Update with domains in TestImpersonate.TEST_URLS if needed from:
28 # https://github.com/lwthiker/curl-impersonate/blob/main/tests/test_impersonate.py
29 domains = [
30 "www.wikimedia.org"
31 "www.wikipedia.org"
32 "www.mozilla.org"
33 "www.apache.org"
34 "www.kernel.org"
35 "git-scm.com"
36 ];
37
38 tls-certs =
39 let
40 # Configure CA with X.509 v3 extensions that would be trusted by curl
41 ca-cert-conf = pkgs.writeText "curl-impersonate-ca.cnf" ''
42 basicConstraints = critical, CA:TRUE
43 subjectKeyIdentifier = hash
44 authorityKeyIdentifier = keyid:always, issuer:always
45 keyUsage = critical, cRLSign, digitalSignature, keyCertSign
46 '';
47
48 # Configure leaf certificate with X.509 v3 extensions that would be trusted
49 # by curl and set subject-alternative names for test domains
50 tls-cert-conf = pkgs.writeText "curl-impersonate-tls.cnf" ''
51 basicConstraints = critical, CA:FALSE
52 subjectKeyIdentifier = hash
53 authorityKeyIdentifier = keyid:always, issuer:always
54 keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment, keyAgreement
55 extendedKeyUsage = critical, serverAuth
56 subjectAltName = @alt_names
57
58 [alt_names]
59 ${lib.concatStringsSep "\n" (lib.imap0 (idx: domain: "DNS.${toString idx} = ${domain}") domains)}
60 '';
61 in
62 pkgs.runCommand "curl-impersonate-test-certs"
63 {
64 nativeBuildInputs = [ pkgs.openssl ];
65 }
66 ''
67 # create CA certificate and key
68 openssl req -newkey rsa:4096 -keyout ca-key.pem -out ca-csr.pem -nodes -subj '/CN=curl-impersonate-ca.nixos.test'
69 openssl x509 -req -sha512 -in ca-csr.pem -key ca-key.pem -out ca.pem -extfile ${ca-cert-conf} -days 36500
70 openssl x509 -in ca.pem -text
71
72 # create server certificate and key
73 openssl req -newkey rsa:4096 -keyout key.pem -out csr.pem -nodes -subj '/CN=curl-impersonate.nixos.test'
74 openssl x509 -req -sha512 -in csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile ${tls-cert-conf} -days 36500
75 openssl x509 -in cert.pem -text
76
77 # output CA cert and server cert and key
78 mkdir -p $out
79 cp key.pem cert.pem ca.pem $out
80 '';
81
82 # Test script
83 curl-impersonate-test =
84 let
85 # Build miniature libcurl client used by test driver
86 minicurl =
87 pkgs.runCommandCC "minicurl"
88 {
89 buildInputs = [ pkgs.curl ];
90 }
91 ''
92 mkdir -p $out/bin
93 $CC -Wall -Werror -o $out/bin/minicurl ${pkgs.curl-impersonate.src}/tests/minicurl.c `curl-config --libs`
94 '';
95 in
96 pkgs.writeShellScript "curl-impersonate-test" ''
97 set -euxo pipefail
98
99 # Test driver requirements
100 export PATH="${
101 with pkgs;
102 lib.makeBinPath [
103 bash
104 coreutils
105 python3Packages.pytest
106 nghttp2
107 tcpdump
108 ]
109 }"
110 export PYTHONPATH="${
111 with pkgs.python3Packages;
112 makePythonPath [
113 pyyaml
114 pytest-asyncio
115 dpkt
116 ts1-signatures
117 ]
118 }"
119
120 # Prepare test root prefix
121 mkdir -p usr/{bin,lib}
122 cp -rs ${pkgs.curl-impersonate}/* ${minicurl}/* usr/
123
124 cp -r ${pkgs.curl-impersonate.src}/tests ./
125
126 # Run tests
127 cd tests
128 pytest . --install-dir ../usr --capture-interface eth1
129 '';
130 in
131 {
132 name = "curl-impersonate";
133
134 meta = with lib.maintainers; {
135 maintainers = [ ];
136 };
137
138 nodes = {
139 web =
140 {
141 nodes,
142 pkgs,
143 lib,
144 config,
145 ...
146 }:
147 {
148 networking.firewall.allowedTCPPorts = [
149 80
150 443
151 ];
152
153 services = {
154 nginx = {
155 enable = true;
156 virtualHosts."curl-impersonate.nixos.test" = {
157 default = true;
158 addSSL = true;
159 sslCertificate = "${tls-certs}/cert.pem";
160 sslCertificateKey = "${tls-certs}/key.pem";
161 };
162 };
163 };
164 };
165
166 curl =
167 {
168 nodes,
169 pkgs,
170 lib,
171 config,
172 ...
173 }:
174 {
175 networking.extraHosts = lib.concatStringsSep "\n" (
176 map (domain: "${nodes.web.networking.primaryIPAddress} ${domain}") domains
177 );
178
179 security.pki.certificateFiles = [ "${tls-certs}/ca.pem" ];
180 };
181 };
182
183 testScript =
184 { nodes, ... }:
185 ''
186 start_all()
187
188 with subtest("Wait for network"):
189 web.systemctl("start network-online.target")
190 curl.systemctl("start network-online.target")
191 web.wait_for_unit("network-online.target")
192 curl.wait_for_unit("network-online.target")
193
194 with subtest("Wait for web server"):
195 web.wait_for_unit("nginx.service")
196 web.wait_for_open_port(443)
197
198 with subtest("Run curl-impersonate tests"):
199 curl.succeed("${curl-impersonate-test}")
200 '';
201 }
202)