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 - We started skipping the test_http2_headers test due to log format differences
23 between the nghttpd2 version in nixpkgs and the outdated one curl-impersonate
24 uses upstream for its tests.
25*/
26
27{ pkgs, lib, ... }:
28let
29 # Update with domains in TestImpersonate.TEST_URLS if needed from:
30 # https://github.com/lwthiker/curl-impersonate/blob/main/tests/test_impersonate.py
31 domains = [
32 "www.wikimedia.org"
33 "www.wikipedia.org"
34 "www.mozilla.org"
35 "www.apache.org"
36 "www.kernel.org"
37 "git-scm.com"
38 ];
39
40 tls-certs =
41 let
42 # Configure CA with X.509 v3 extensions that would be trusted by curl
43 ca-cert-conf = pkgs.writeText "curl-impersonate-ca.cnf" ''
44 basicConstraints = critical, CA:TRUE
45 subjectKeyIdentifier = hash
46 authorityKeyIdentifier = keyid:always, issuer:always
47 keyUsage = critical, cRLSign, digitalSignature, keyCertSign
48 '';
49
50 # Configure leaf certificate with X.509 v3 extensions that would be trusted
51 # by curl and set subject-alternative names for test domains
52 tls-cert-conf = pkgs.writeText "curl-impersonate-tls.cnf" ''
53 basicConstraints = critical, CA:FALSE
54 subjectKeyIdentifier = hash
55 authorityKeyIdentifier = keyid:always, issuer:always
56 keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment, keyAgreement
57 extendedKeyUsage = critical, serverAuth
58 subjectAltName = @alt_names
59
60 [alt_names]
61 ${lib.concatStringsSep "\n" (lib.imap0 (idx: domain: "DNS.${toString idx} = ${domain}") domains)}
62 '';
63 in
64 pkgs.runCommand "curl-impersonate-test-certs"
65 {
66 nativeBuildInputs = [ pkgs.openssl ];
67 }
68 ''
69 # create CA certificate and key
70 openssl req -newkey rsa:4096 -keyout ca-key.pem -out ca-csr.pem -nodes -subj '/CN=curl-impersonate-ca.nixos.test'
71 openssl x509 -req -sha512 -in ca-csr.pem -key ca-key.pem -out ca.pem -extfile ${ca-cert-conf} -days 36500
72 openssl x509 -in ca.pem -text
73
74 # create server certificate and key
75 openssl req -newkey rsa:4096 -keyout key.pem -out csr.pem -nodes -subj '/CN=curl-impersonate.nixos.test'
76 openssl x509 -req -sha512 -in csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile ${tls-cert-conf} -days 36500
77 openssl x509 -in cert.pem -text
78
79 # output CA cert and server cert and key
80 mkdir -p $out
81 cp key.pem cert.pem ca.pem $out
82 '';
83
84 # Test script
85 curl-impersonate-test =
86 let
87 # Build miniature libcurl client used by test driver
88 minicurl =
89 pkgs.runCommandCC "minicurl"
90 {
91 buildInputs = [ pkgs.curl ];
92 }
93 ''
94 mkdir -p $out/bin
95 $CC -Wall -Werror -o $out/bin/minicurl ${pkgs.curl-impersonate.src}/tests/minicurl.c `curl-config --libs`
96 '';
97 in
98 pkgs.writeShellScript "curl-impersonate-test" ''
99 set -euxo pipefail
100
101 # Test driver requirements
102 export PATH="${
103 with pkgs;
104 lib.makeBinPath [
105 bash
106 coreutils
107 python3Packages.pytest
108 nghttp2
109 tcpdump
110 ]
111 }"
112 export PYTHONPATH="${
113 with pkgs.python3Packages;
114 makePythonPath [
115 pyyaml
116 pytest-asyncio
117 dpkt
118 ts1-signatures
119 ]
120 }"
121
122 # Prepare test root prefix
123 mkdir -p usr/{bin,lib}
124 cp -rs ${pkgs.curl-impersonate}/* ${minicurl}/* usr/
125
126 cp -r ${pkgs.curl-impersonate.src}/tests ./
127
128 # Run tests
129 cd tests
130 pytest . --install-dir ../usr --capture-interface eth1 --exitfirst -k 'not test_http2_headers'
131 '';
132in
133{
134 name = "curl-impersonate";
135
136 meta = with lib.maintainers; {
137 maintainers = [ ];
138 };
139
140 nodes = {
141 web =
142 {
143 nodes,
144 pkgs,
145 lib,
146 config,
147 ...
148 }:
149 {
150 networking.firewall.allowedTCPPorts = [
151 80
152 443
153 ];
154
155 services = {
156 nginx = {
157 enable = true;
158 virtualHosts."curl-impersonate.nixos.test" = {
159 default = true;
160 addSSL = true;
161 sslCertificate = "${tls-certs}/cert.pem";
162 sslCertificateKey = "${tls-certs}/key.pem";
163 };
164 };
165 };
166 };
167
168 curl =
169 {
170 nodes,
171 pkgs,
172 lib,
173 config,
174 ...
175 }:
176 {
177 networking.extraHosts = lib.concatStringsSep "\n" (
178 map (domain: "${nodes.web.networking.primaryIPAddress} ${domain}") domains
179 );
180
181 security.pki.certificateFiles = [ "${tls-certs}/ca.pem" ];
182 };
183 };
184
185 testScript =
186 { nodes, ... }:
187 ''
188 start_all()
189
190 with subtest("Wait for network"):
191 web.systemctl("start network-online.target")
192 curl.systemctl("start network-online.target")
193 web.wait_for_unit("network-online.target")
194 curl.wait_for_unit("network-online.target")
195
196 with subtest("Wait for web server"):
197 web.wait_for_unit("nginx.service")
198 web.wait_for_open_port(443)
199
200 with subtest("Run curl-impersonate tests"):
201 curl.succeed("${curl-impersonate-test}")
202 '';
203}