/* Test suite for curl-impersonate Abstract: Uses the test suite from the curl-impersonate source repo which: 1. Performs requests with libcurl and captures the TLS client-hello packets with tcpdump to compare against known-good signatures 2. Spins up an nghttpd2 server to test client HTTP/2 headers against known-good headers See https://github.com/lwthiker/curl-impersonate/tree/main/tests/signatures for details. Notes: - We need to have our own web server running because the tests expect to be able to hit domains like wikipedia.org and the sandbox has no internet - We need to be able to do (verifying) TLS handshakes without internet access. We do that by creating a trusted CA and issuing a cert that includes all of the test domains as subject-alternative names and then spoofs the hostnames in /etc/hosts. */ import ./make-test-python.nix ({ pkgs, lib, ... }: let # Update with domains in TestImpersonate.TEST_URLS if needed from: # https://github.com/lwthiker/curl-impersonate/blob/main/tests/test_impersonate.py domains = [ "www.wikimedia.org" "www.wikipedia.org" "www.mozilla.org" "www.apache.org" "www.kernel.org" "git-scm.com" ]; tls-certs = let # Configure CA with X.509 v3 extensions that would be trusted by curl ca-cert-conf = pkgs.writeText "curl-impersonate-ca.cnf" '' basicConstraints = critical, CA:TRUE subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always, issuer:always keyUsage = critical, cRLSign, digitalSignature, keyCertSign ''; # Configure leaf certificate with X.509 v3 extensions that would be trusted # by curl and set subject-alternative names for test domains tls-cert-conf = pkgs.writeText "curl-impersonate-tls.cnf" '' basicConstraints = critical, CA:FALSE subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always, issuer:always keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment, keyAgreement extendedKeyUsage = critical, serverAuth subjectAltName = @alt_names [alt_names] ${lib.concatStringsSep "\n" (lib.imap0 (idx: domain: "DNS.${toString idx} = ${domain}") domains)} ''; in pkgs.runCommand "curl-impersonate-test-certs" { nativeBuildInputs = [ pkgs.openssl ]; } '' # create CA certificate and key openssl req -newkey rsa:4096 -keyout ca-key.pem -out ca-csr.pem -nodes -subj '/CN=curl-impersonate-ca.nixos.test' openssl x509 -req -sha512 -in ca-csr.pem -key ca-key.pem -out ca.pem -extfile ${ca-cert-conf} -days 36500 openssl x509 -in ca.pem -text # create server certificate and key openssl req -newkey rsa:4096 -keyout key.pem -out csr.pem -nodes -subj '/CN=curl-impersonate.nixos.test' openssl x509 -req -sha512 -in csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile ${tls-cert-conf} -days 36500 openssl x509 -in cert.pem -text # output CA cert and server cert and key mkdir -p $out cp key.pem cert.pem ca.pem $out ''; # Test script curl-impersonate-test = let # Build miniature libcurl client used by test driver minicurl = pkgs.runCommandCC "minicurl" { buildInputs = [ pkgs.curl ]; } '' mkdir -p $out/bin $CC -Wall -Werror -o $out/bin/minicurl ${pkgs.curl-impersonate.src}/tests/minicurl.c `curl-config --libs` ''; in pkgs.writeShellScript "curl-impersonate-test" '' set -euxo pipefail # Test driver requirements export PATH="${with pkgs; lib.makeBinPath [ bash coreutils python3Packages.pytest nghttp2 tcpdump ]}" export PYTHONPATH="${with pkgs.python3Packages; makePythonPath [ pyyaml pytest-asyncio dpkt ]}" # Prepare test root prefix mkdir -p usr/{bin,lib} cp -rs ${pkgs.curl-impersonate}/* ${minicurl}/* usr/ cp -r ${pkgs.curl-impersonate.src}/tests ./ # Run tests cd tests pytest . --install-dir ../usr --capture-interface eth1 ''; in { name = "curl-impersonate"; meta = with lib.maintainers; { maintainers = [ lilyinstarlight ]; }; nodes = { web = { nodes, pkgs, lib, config, ... }: { networking.firewall.allowedTCPPorts = [ 80 443 ]; services = { nginx = { enable = true; virtualHosts."curl-impersonate.nixos.test" = { default = true; addSSL = true; sslCertificate = "${tls-certs}/cert.pem"; sslCertificateKey = "${tls-certs}/key.pem"; }; }; }; }; curl = { nodes, pkgs, lib, config, ... }: { networking.extraHosts = lib.concatStringsSep "\n" (map (domain: "${nodes.web.networking.primaryIPAddress} ${domain}") domains); security.pki.certificateFiles = [ "${tls-certs}/ca.pem" ]; }; }; testScript = { nodes, ... }: '' start_all() with subtest("Wait for network"): web.systemctl("start network-online.target") curl.systemctl("start network-online.target") web.wait_for_unit("network-online.target") curl.wait_for_unit("network-online.target") with subtest("Wait for web server"): web.wait_for_unit("nginx.service") web.wait_for_open_port(443) with subtest("Run curl-impersonate tests"): curl.succeed("${curl-impersonate-test}") ''; })