1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 domain = "example.test";
9in
10{
11 name = "http01-builtin";
12 meta = {
13 maintainers = lib.teams.acme.members;
14 # Hard timeout in seconds. Average run time is about 90 seconds.
15 timeout = 300;
16 };
17
18 nodes = {
19 # The fake ACME server which will respond to client requests
20 acme =
21 { nodes, ... }:
22 {
23 imports = [ ../common/acme/server ];
24 };
25
26 builtin =
27 { nodes, config, ... }:
28 {
29 imports = [ ../common/acme/client ];
30 networking.domain = domain;
31 networking.firewall.allowedTCPPorts = [ 80 ];
32
33 # OpenSSL will be used for more thorough certificate validation
34 environment.systemPackages = [ pkgs.openssl ];
35
36 security.acme.certs."${config.networking.fqdn}" = {
37 listenHTTP = ":80";
38 };
39
40 systemd.targets."renew-triggered" = {
41 wantedBy = [ "acme-order-renew-${config.networking.fqdn}.service" ];
42 after = [ "acme-order-renew-${config.networking.fqdn}.service" ];
43 unitConfig.RefuseManualStart = true;
44 };
45
46 specialisation = {
47 renew.configuration = {
48 # Pebble provides 5 year long certs,
49 # needs to be higher than that to test renewal
50 security.acme.certs."${config.networking.fqdn}".validMinDays = 9999;
51 };
52
53 accountchange.configuration = {
54 security.acme.certs."${config.networking.fqdn}".email = "admin@example.test";
55 };
56
57 keytype.configuration = {
58 security.acme.certs."${config.networking.fqdn}".keyType = "ec384";
59 };
60
61 # Perform http-01 test again, but using the pre-24.05 account hashing
62 # (see https://github.com/NixOS/nixpkgs/pull/317257)
63 # The hash is deterministic in this case - only based on keyType and email.
64 # Note: This test is making the assumption that the acme module will create
65 # the account directory regardless of internet connectivity or server reachability.
66 legacy_account_hash.configuration = {
67 security.acme.defaults.server = lib.mkForce null;
68 };
69
70 ocsp_stapling.configuration = {
71 security.acme.certs."${config.networking.fqdn}".ocspMustStaple = true;
72 };
73
74 preservation.configuration = { };
75
76 add_cert_and_domain.configuration = {
77 security.acme.certs = {
78 "${config.networking.fqdn}" = {
79 extraDomainNames = [
80 "builtin-alt.${domain}"
81 ];
82 };
83 # We can assume that if renewal succeeds then the account creation leader
84 # logic is working, since only one service could bind to port 80 at the same time.
85 "builtin-2.${domain}".listenHTTP = ":80";
86 };
87 # To make sure it's the account creation leader that is doing the work.
88 security.acme.maxConcurrentRenewals = 10;
89 };
90
91 concurrency.configuration = {
92 # As above, relying on port binding behaviour to assert that concurrency limit
93 # prevents > 1 service running at a time.
94 security.acme.maxConcurrentRenewals = 1;
95 security.acme.certs = {
96 "${config.networking.fqdn}" = {
97 extraDomainNames = [
98 "builtin-alt.${domain}"
99 ];
100 };
101 "builtin-2.${domain}" = {
102 extraDomainNames = [ "builtin-2-alt.${domain}" ];
103 listenHTTP = ":80";
104 };
105 "builtin-3.${domain}".listenHTTP = ":80";
106 };
107 };
108
109 csr.configuration =
110 let
111 conf = pkgs.writeText "openssl.csr.conf" ''
112 [req]
113 default_bits = 2048
114 prompt = no
115 default_md = sha256
116 req_extensions = req_ext
117 distinguished_name = dn
118
119 [ dn ]
120 CN = ${config.networking.fqdn}
121
122 [ req_ext ]
123 subjectAltName = @alt_names
124
125 [ alt_names ]
126 DNS.1 = ${config.networking.fqdn}
127 '';
128 csrData =
129 pkgs.runCommandNoCC "csr-and-key"
130 {
131 buildInputs = [ pkgs.openssl ];
132 }
133 ''
134 mkdir -p $out
135 openssl req -new -newkey rsa:2048 -nodes \
136 -keyout $out/key.pem \
137 -out $out/request.csr \
138 -config ${conf}
139 '';
140 in
141 {
142 security.acme.certs."${config.networking.fqdn}" = {
143 csr = "${csrData}/request.csr";
144 csrKey = "${csrData}/key.pem";
145 };
146 };
147 };
148 };
149 };
150
151 testScript =
152 { nodes, ... }:
153 let
154 certName = nodes.builtin.networking.fqdn;
155 caDomain = nodes.acme.test-support.acme.caDomain;
156 in
157 ''
158 ${(import ./utils.nix).pythonUtils}
159
160 domain = "${domain}"
161 cert = "${certName}"
162 cert2 = "builtin-2." + domain
163 cert3 = "builtin-3." + domain
164 legacy_account_dir = "/var/lib/acme/.lego/accounts/1ccf607d9aa280e9af00"
165
166 acme.start()
167 wait_for_running(acme)
168 acme.wait_for_open_port(443)
169
170 with subtest("Boot and acquire a new cert"):
171 builtin.start()
172 wait_for_running(builtin)
173
174 check_issuer(builtin, cert, "pebble")
175 check_domain(builtin, cert, cert)
176
177 with subtest("Validate permissions"):
178 check_permissions(builtin, cert, "acme")
179
180 with subtest("Check renewal behaviour"):
181 # First, test no-op behaviour
182 hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem")
183 # old_hash will be used in the preservation tests later
184 old_hash = hash
185 builtin.succeed(f"systemctl start acme-{cert}.service")
186 builtin.succeed(f"systemctl start acme-order-renew-{cert}.service")
187 builtin.wait_for_unit("renew-triggered.target")
188
189 hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem")
190 assert hash == hash_after, "Certificate was unexpectedly changed"
191
192 builtin.succeed("systemctl stop renew-triggered.target")
193 switch_to(builtin, "renew")
194 builtin.wait_for_unit("renew-triggered.target")
195
196 check_issuer(builtin, cert, "pebble")
197 hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem | tee /dev/stderr")
198 assert hash != hash_after, "Certificate was not renewed"
199
200 check_permissions(builtin, cert, "acme")
201
202 with subtest("Handles email change correctly"):
203 hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem")
204
205 builtin.succeed("systemctl stop renew-triggered.target")
206 switch_to(builtin, "accountchange")
207 builtin.wait_for_unit("renew-triggered.target")
208
209 check_issuer(builtin, cert, "pebble")
210 # Check that there are now 2 account directories
211 builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2")
212 hash_after = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem")
213 # Has to do a full run to register account, which creates new certs.
214 assert hash != hash_after, "Certificate was not renewed"
215 # Remove the new account directory
216 builtin.succeed(
217 "cd /var/lib/acme/.lego/accounts"
218 " && ls -1 --sort=time | tee /dev/stderr | head -1 | xargs rm -rf"
219 )
220 # old_hash will be used in the preservation tests later
221 old_hash = hash_after
222
223 check_permissions(builtin, cert, "acme")
224
225 with subtest("Correctly implements OCSP stapling"):
226 check_stapling(builtin, cert, "${caDomain}", fail=True)
227
228 builtin.succeed("systemctl stop renew-triggered.target")
229 switch_to(builtin, "ocsp_stapling")
230 builtin.wait_for_unit("renew-triggered.target")
231
232 check_stapling(builtin, cert, "${caDomain}")
233 check_permissions(builtin, cert, "acme")
234
235 with subtest("Handles keyType change correctly"):
236 check_key_bits(builtin, cert, 256)
237
238 builtin.succeed("systemctl stop renew-triggered.target")
239 switch_to(builtin, "keytype")
240 builtin.wait_for_unit("renew-triggered.target")
241
242 check_key_bits(builtin, cert, 384)
243 # keyType is part of the accountHash, thus a new account will be created
244 builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2")
245 check_permissions(builtin, cert, "acme")
246
247 with subtest("Reuses generated, valid certs from previous configurations"):
248 # Right now, the hash should not match due to the previous test
249 hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem | tee /dev/stderr")
250 assert hash != old_hash, "Expected certificate to differ"
251
252 builtin.succeed("systemctl stop renew-triggered.target")
253 switch_to(builtin, "preservation")
254 builtin.wait_for_unit("renew-triggered.target")
255
256 hash = builtin.succeed(f"sha256sum /var/lib/acme/{cert}/cert.pem | tee /dev/stderr")
257 assert hash == old_hash, "Expected certificate to match from older configuration"
258 check_permissions(builtin, cert, "acme")
259
260 with subtest("Add a new cert, extend existing cert domains"):
261 check_domain(builtin, cert, f"builtin-alt.{domain}", fail=True)
262
263 builtin.succeed("systemctl stop renew-triggered.target")
264 switch_to(builtin, "add_cert_and_domain")
265 builtin.wait_for_unit("renew-triggered.target")
266
267 check_issuer(builtin, cert, "pebble")
268 check_domain(builtin, cert, f"builtin-alt.{domain}")
269 check_issuer(builtin, cert2, "pebble")
270 check_domain(builtin, cert2, cert2)
271 # There should not be a new account folder created
272 builtin.succeed("test $(ls -1 /var/lib/acme/.lego/accounts | tee /dev/stderr | wc -l) -eq 2")
273 check_permissions(builtin, cert, "acme")
274 check_permissions(builtin, cert2, "acme")
275
276 with subtest("Check account hashing compatibility with pre-24.05 settings"):
277 builtin.succeed("systemctl stop renew-triggered.target")
278 switch_to(builtin, "legacy_account_hash"
279 )
280 builtin.wait_for_unit("renew-triggered.target")
281
282 builtin.succeed(f"stat {legacy_account_dir} > /dev/stderr && rm -rf {legacy_account_dir}")
283 check_permissions(builtin, cert, "acme")
284
285 with subtest("Ensure concurrency limits work"):
286 builtin.succeed("systemctl stop renew-triggered.target")
287 switch_to(builtin, "concurrency")
288 builtin.wait_for_unit("renew-triggered.target")
289
290 check_issuer(builtin, cert3, "pebble")
291 check_domain(builtin, cert3, cert3)
292 check_permissions(builtin, cert, "acme")
293
294 with subtest("Can renew using a CSR"):
295 builtin.succeed(f"systemctl stop acme-{cert}.service")
296 builtin.succeed(f"systemctl clean acme-{cert}.service --what=state")
297
298 builtin.succeed("systemctl stop renew-triggered.target")
299 switch_to(builtin, "csr")
300 builtin.wait_for_unit("renew-triggered.target")
301
302 check_issuer(builtin, cert, "pebble")
303
304 with subtest("Generate self-signed certs"):
305 acme.shutdown()
306
307 check_issuer(builtin, cert, "pebble")
308
309 builtin.succeed(f"systemctl stop acme-{cert}.service")
310 builtin.succeed(f"systemctl clean acme-{cert}.service --what=state")
311 builtin.succeed(f"systemctl start acme-{cert}.service")
312
313 check_issuer(builtin, cert, "minica")
314 check_domain(builtin, cert, cert)
315
316 with subtest("Validate permissions (self-signed)"):
317 check_permissions(builtin, cert, "acme")
318
319 '';
320}