xref: /openbsd-src/usr.sbin/acme-client/main.c (revision d7c05bd5f83de097d211cbdbff6f9049fbd68185)
1 /*	$Id: main.c,v 1.56 2024/06/19 13:13:25 claudio Exp $ */
2 /*
3  * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv>
4  *
5  * Permission to use, copy, modify, and distribute this software for any
6  * purpose with or without fee is hereby granted, provided that the above
7  * copyright notice and this permission notice appear in all copies.
8  *
9  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
10  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
12  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16  */
17 
18 #include <sys/socket.h>
19 
20 #include <ctype.h>
21 #include <err.h>
22 #include <libgen.h>
23 #include <locale.h>
24 #include <signal.h>
25 #include <stdarg.h>
26 #include <stdio.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <unistd.h>
30 
31 #include "extern.h"
32 #include "parse.h"
33 
34 #define WWW_DIR "/var/www/acme"
35 #define CONF_FILE "/etc/acme-client.conf"
36 
37 int		 verbose;
38 enum comp 	 proccomp;
39 
40 int
main(int argc,char * argv[])41 main(int argc, char *argv[])
42 {
43 	const char	 **alts = NULL;
44 	char		 *certdir = NULL;
45 	char		 *chngdir = NULL, *auth = NULL;
46 	char		 *conffile = CONF_FILE;
47 	char		 *tmps, *tmpsd;
48 	int		  key_fds[2], acct_fds[2], chng_fds[2], cert_fds[2];
49 	int		  file_fds[2], dns_fds[2], rvk_fds[2];
50 	int		  force = 0;
51 	int		  c, rc, revocate = 0;
52 	int		  popts = 0;
53 	pid_t		  pids[COMP__MAX];
54 	size_t		  i, altsz, ne;
55 
56 	struct acme_conf	*conf = NULL;
57 	struct authority_c	*authority = NULL;
58 	struct domain_c		*domain = NULL;
59 	struct altname_c	*ac;
60 
61 	if (setlocale(LC_CTYPE, "C") == NULL)
62 		errx(1, "setlocale");
63 
64 	while ((c = getopt(argc, argv, "Fnrvf:")) != -1)
65 		switch (c) {
66 		case 'F':
67 			force = 1;
68 			break;
69 		case 'f':
70 			if ((conffile = strdup(optarg)) == NULL)
71 				err(EXIT_FAILURE, "strdup");
72 			break;
73 		case 'n':
74 			popts |= ACME_OPT_CHECK;
75 			break;
76 		case 'r':
77 			revocate = 1;
78 			break;
79 		case 'v':
80 			verbose = verbose ? 2 : 1;
81 			popts |= ACME_OPT_VERBOSE;
82 			break;
83 		default:
84 			goto usage;
85 		}
86 
87 	if (getuid() != 0)
88 		errx(EXIT_FAILURE, "must be run as root");
89 
90 	/* parse config file */
91 	if ((conf = parse_config(conffile, popts)) == NULL)
92 		return EXIT_FAILURE;
93 
94 	argc -= optind;
95 	argv += optind;
96 	if (argc != 1)
97 		goto usage;
98 
99 	if ((domain = domain_find_handle(conf, argv[0])) == NULL)
100 		errx(EXIT_FAILURE, "domain %s not found", argv[0]);
101 
102 	argc--;
103 	argv++;
104 
105 	/*
106 	 * The parser enforces that at least cert or fullchain is set.
107 	 * XXX Test if cert, chain and fullchain have the same dirname?
108 	 */
109 	tmps = domain->cert ? domain->cert : domain->fullchain;
110 	if ((tmps = strdup(tmps)) == NULL)
111 		err(EXIT_FAILURE, "strdup");
112 	if ((tmpsd = dirname(tmps)) == NULL)
113 		err(EXIT_FAILURE, "dirname");
114 	if ((certdir = strdup(tmpsd)) == NULL)
115 		err(EXIT_FAILURE, "strdup");
116 	free(tmps);
117 	tmps = tmpsd = NULL;
118 
119 
120 	/* chain or fullchain can be relative paths according */
121 	if (domain->chain && domain->chain[0] != '/') {
122 		if (asprintf(&tmps, "%s/%s", certdir, domain->chain) == -1)
123 			err(EXIT_FAILURE, "asprintf");
124 		free(domain->chain);
125 		domain->chain = tmps;
126 		tmps = NULL;
127 	}
128 	if (domain->fullchain && domain->fullchain[0] != '/') {
129 		if (asprintf(&tmps, "%s/%s", certdir, domain->fullchain) == -1)
130 			err(EXIT_FAILURE, "asprintf");
131 		free(domain->fullchain);
132 		domain->fullchain = tmps;
133 		tmps = NULL;
134 	}
135 
136 	if ((auth = domain->auth) == NULL) {
137 		/* use the first authority from the config as default XXX */
138 		authority = authority_find0(conf);
139 		if (authority == NULL)
140 			errx(EXIT_FAILURE, "no authorities configured");
141 	} else {
142 		authority = authority_find(conf, auth);
143 		if (authority == NULL)
144 			errx(EXIT_FAILURE, "authority %s not found", auth);
145 	}
146 
147 	if ((chngdir = domain->challengedir) == NULL)
148 		if ((chngdir = strdup(WWW_DIR)) == NULL)
149 			err(EXIT_FAILURE, "strdup");
150 
151 	/*
152 	 * Do some quick checks to see if our paths exist.
153 	 * This will be done in the children, but we might as well check
154 	 * now before the fork.
155 	 * XXX maybe use conf_check_file() from parse.y
156 	 */
157 
158 	ne = 0;
159 
160 	if (access(certdir, R_OK) == -1) {
161 		warnx("%s: cert directory must exist", certdir);
162 		ne++;
163 	}
164 
165 	if (access(chngdir, R_OK) == -1) {
166 		warnx("%s: challenge directory must exist", chngdir);
167 		ne++;
168 	}
169 
170 	if (ne > 0)
171 		return EXIT_FAILURE;
172 
173 	if (popts & ACME_OPT_CHECK)
174 		return EXIT_SUCCESS;
175 
176 	/* Set the zeroth altname as our domain. */
177 	altsz = domain->altname_count + 1;
178 	alts = calloc(altsz, sizeof(char *));
179 	if (alts == NULL)
180 		err(EXIT_FAILURE, "calloc");
181 	alts[0] = domain->domain;
182 	i = 1;
183 	/* XXX get rid of alts[] later */
184 	TAILQ_FOREACH(ac, &domain->altname_list, entry)
185 		alts[i++] = ac->domain;
186 
187 	/*
188 	 * Open channels between our components.
189 	 */
190 
191 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, key_fds) == -1)
192 		err(EXIT_FAILURE, "socketpair");
193 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, acct_fds) == -1)
194 		err(EXIT_FAILURE, "socketpair");
195 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, chng_fds) == -1)
196 		err(EXIT_FAILURE, "socketpair");
197 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, cert_fds) == -1)
198 		err(EXIT_FAILURE, "socketpair");
199 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, file_fds) == -1)
200 		err(EXIT_FAILURE, "socketpair");
201 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, dns_fds) == -1)
202 		err(EXIT_FAILURE, "socketpair");
203 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, rvk_fds) == -1)
204 		err(EXIT_FAILURE, "socketpair");
205 
206 	signal(SIGPIPE, SIG_IGN);
207 
208 	/* Start with the network-touching process. */
209 
210 	if ((pids[COMP_NET] = fork()) == -1)
211 		err(EXIT_FAILURE, "fork");
212 
213 	if (pids[COMP_NET] == 0) {
214 		proccomp = COMP_NET;
215 		close(key_fds[0]);
216 		close(acct_fds[0]);
217 		close(chng_fds[0]);
218 		close(cert_fds[0]);
219 		close(file_fds[0]);
220 		close(file_fds[1]);
221 		close(dns_fds[0]);
222 		close(rvk_fds[0]);
223 		c = netproc(key_fds[1], acct_fds[1],
224 		    chng_fds[1], cert_fds[1],
225 		    dns_fds[1], rvk_fds[1],
226 		    revocate, authority,
227 		    (const char *const *)alts, altsz);
228 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
229 	}
230 
231 	close(key_fds[1]);
232 	close(acct_fds[1]);
233 	close(chng_fds[1]);
234 	close(cert_fds[1]);
235 	close(dns_fds[1]);
236 	close(rvk_fds[1]);
237 
238 	/* Now the key-touching component. */
239 
240 	if ((pids[COMP_KEY] = fork()) == -1)
241 		err(EXIT_FAILURE, "fork");
242 
243 	if (pids[COMP_KEY] == 0) {
244 		proccomp = COMP_KEY;
245 		close(cert_fds[0]);
246 		close(dns_fds[0]);
247 		close(rvk_fds[0]);
248 		close(acct_fds[0]);
249 		close(chng_fds[0]);
250 		close(file_fds[0]);
251 		close(file_fds[1]);
252 		c = keyproc(key_fds[0], domain->key,
253 		    (const char **)alts, altsz,
254 		    domain->keytype);
255 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
256 	}
257 
258 	close(key_fds[0]);
259 
260 	/* The account-touching component. */
261 
262 	if ((pids[COMP_ACCOUNT] = fork()) == -1)
263 		err(EXIT_FAILURE, "fork");
264 
265 	if (pids[COMP_ACCOUNT] == 0) {
266 		proccomp = COMP_ACCOUNT;
267 		close(cert_fds[0]);
268 		close(dns_fds[0]);
269 		close(rvk_fds[0]);
270 		close(chng_fds[0]);
271 		close(file_fds[0]);
272 		close(file_fds[1]);
273 		c = acctproc(acct_fds[0], authority->account,
274 		    authority->keytype);
275 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
276 	}
277 
278 	close(acct_fds[0]);
279 
280 	/* The challenge-accepting component. */
281 
282 	if ((pids[COMP_CHALLENGE] = fork()) == -1)
283 		err(EXIT_FAILURE, "fork");
284 
285 	if (pids[COMP_CHALLENGE] == 0) {
286 		proccomp = COMP_CHALLENGE;
287 		close(cert_fds[0]);
288 		close(dns_fds[0]);
289 		close(rvk_fds[0]);
290 		close(file_fds[0]);
291 		close(file_fds[1]);
292 		c = chngproc(chng_fds[0], chngdir);
293 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
294 	}
295 
296 	close(chng_fds[0]);
297 
298 	/* The certificate-handling component. */
299 
300 	if ((pids[COMP_CERT] = fork()) == -1)
301 		err(EXIT_FAILURE, "fork");
302 
303 	if (pids[COMP_CERT] == 0) {
304 		proccomp = COMP_CERT;
305 		close(dns_fds[0]);
306 		close(rvk_fds[0]);
307 		close(file_fds[1]);
308 		c = certproc(cert_fds[0], file_fds[0]);
309 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
310 	}
311 
312 	close(cert_fds[0]);
313 	close(file_fds[0]);
314 
315 	/* The certificate-handling component. */
316 
317 	if ((pids[COMP_FILE] = fork()) == -1)
318 		err(EXIT_FAILURE, "fork");
319 
320 	if (pids[COMP_FILE] == 0) {
321 		proccomp = COMP_FILE;
322 		close(dns_fds[0]);
323 		close(rvk_fds[0]);
324 		c = fileproc(file_fds[1], certdir, domain->cert, domain->chain,
325 		    domain->fullchain);
326 		/*
327 		 * This is different from the other processes in that it
328 		 * can return 2 if the certificates were updated.
329 		 */
330 		exit(c > 1 ? 2 : (c ? EXIT_SUCCESS : EXIT_FAILURE));
331 	}
332 
333 	close(file_fds[1]);
334 
335 	/* The DNS lookup component. */
336 
337 	if ((pids[COMP_DNS] = fork()) == -1)
338 		err(EXIT_FAILURE, "fork");
339 
340 	if (pids[COMP_DNS] == 0) {
341 		proccomp = COMP_DNS;
342 		close(rvk_fds[0]);
343 		c = dnsproc(dns_fds[0]);
344 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
345 	}
346 
347 	close(dns_fds[0]);
348 
349 	/* The expiration component. */
350 
351 	if ((pids[COMP_REVOKE] = fork()) == -1)
352 		err(EXIT_FAILURE, "fork");
353 
354 	if (pids[COMP_REVOKE] == 0) {
355 		proccomp = COMP_REVOKE;
356 		c = revokeproc(rvk_fds[0], domain->cert != NULL ? domain->cert :
357 		    domain->fullchain, force, revocate,
358 		    (const char *const *)alts, altsz);
359 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
360 	}
361 
362 	close(rvk_fds[0]);
363 
364 	/* Jail: sandbox, file-system, user. */
365 
366 	if (pledge("stdio", NULL) == -1)
367 		err(EXIT_FAILURE, "pledge");
368 
369 	/*
370 	 * Collect our subprocesses.
371 	 * Require that they both have exited cleanly.
372 	 */
373 
374 	rc = checkexit(pids[COMP_KEY], COMP_KEY) +
375 	    checkexit(pids[COMP_CERT], COMP_CERT) +
376 	    checkexit(pids[COMP_NET], COMP_NET) +
377 	    checkexit_ext(&c, pids[COMP_FILE], COMP_FILE) +
378 	    checkexit(pids[COMP_ACCOUNT], COMP_ACCOUNT) +
379 	    checkexit(pids[COMP_CHALLENGE], COMP_CHALLENGE) +
380 	    checkexit(pids[COMP_DNS], COMP_DNS) +
381 	    checkexit(pids[COMP_REVOKE], COMP_REVOKE);
382 
383 	return rc != COMP__MAX ? EXIT_FAILURE : (c == 2 ? EXIT_SUCCESS : 2);
384 usage:
385 	fprintf(stderr,
386 	    "usage: acme-client [-Fnrv] [-f configfile] handle\n");
387 	return EXIT_FAILURE;
388 }
389