xref: /openbsd-src/usr.sbin/acme-client/main.c (revision d1df930ffab53da22f3324c32bed7ac5709915e6)
1 /*	$Id: main.c,v 1.41 2018/07/30 09:59:03 benno 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 <stdarg.h>
24 #include <stdio.h>
25 #include <stdlib.h>
26 #include <string.h>
27 #include <unistd.h>
28 
29 #include "extern.h"
30 #include "parse.h"
31 
32 #define WWW_DIR "/var/www/acme"
33 #define CONF_FILE "/etc/acme-client.conf"
34 
35 int
36 main(int argc, char *argv[])
37 {
38 	const char	 **alts = NULL;
39 	char		 *certdir = NULL, *certfile = NULL;
40 	char		 *chainfile = NULL, *fullchainfile = NULL;
41 	char		 *acctkey = NULL;
42 	char		 *chngdir = NULL, *auth = NULL;
43 	char		 *conffile = CONF_FILE;
44 	int		  key_fds[2], acct_fds[2], chng_fds[2], cert_fds[2];
45 	int		  file_fds[2], dns_fds[2], rvk_fds[2];
46 	int		  force = 0;
47 	int		  c, rc, revocate = 0;
48 	int		  popts = 0;
49 	pid_t		  pids[COMP__MAX];
50 	extern int	  verbose;
51 	extern enum comp  proccomp;
52 	size_t		  i, altsz, ne;
53 
54 	struct acme_conf	*conf = NULL;
55 	struct authority_c	*authority = NULL;
56 	struct domain_c		*domain = NULL;
57 	struct altname_c	*ac;
58 
59 	while ((c = getopt(argc, argv, "ADFnrvf:")) != -1)
60 		switch (c) {
61 		case 'A':
62 			popts |= ACME_OPT_NEWACCT;
63 			break;
64 		case 'D':
65 			popts |= ACME_OPT_NEWDKEY;
66 			break;
67 		case 'F':
68 			force = 1;
69 			break;
70 		case 'f':
71 			if ((conffile = strdup(optarg)) == NULL)
72 				err(EXIT_FAILURE, "strdup");
73 			break;
74 		case 'n':
75 			popts |= ACME_OPT_CHECK;
76 			break;
77 		case 'r':
78 			revocate = 1;
79 			break;
80 		case 'v':
81 			verbose = verbose ? 2 : 1;
82 			popts |= ACME_OPT_VERBOSE;
83 			break;
84 		default:
85 			goto usage;
86 		}
87 
88 	if (getuid() != 0)
89 		errx(EXIT_FAILURE, "must be run as root");
90 
91 	/* parse config file */
92 	if ((conf = parse_config(conffile, popts)) == NULL)
93 		return EXIT_FAILURE;
94 
95 	argc -= optind;
96 	argv += optind;
97 	if (argc != 1)
98 		goto usage;
99 
100 	if ((domain = domain_find(conf, argv[0])) == NULL)
101 		errx(EXIT_FAILURE, "domain %s not found", argv[0]);
102 
103 	argc--;
104 	argv++;
105 
106 	if (domain->cert != NULL) {
107 		if ((certdir = dirname(domain->cert)) != NULL) {
108 			if ((certdir = strdup(certdir)) == NULL)
109 				err(EXIT_FAILURE, "strdup");
110 		} else
111 			err(EXIT_FAILURE, "dirname");
112 	} else {
113 		/* the parser enforces that at least cert or fullchain is set */
114 		if ((certdir = dirname(domain->fullchain)) != NULL) {
115 			if ((certdir = strdup(certdir)) == NULL)
116 				err(EXIT_FAILURE, "strdup");
117 		} else
118 			err(EXIT_FAILURE, "dirname");
119 
120 	}
121 
122 	if (domain->cert != NULL) {
123 		if ((certfile = basename(domain->cert)) != NULL) {
124 			if ((certfile = strdup(certfile)) == NULL)
125 				err(EXIT_FAILURE, "strdup");
126 		} else
127 			err(EXIT_FAILURE, "basename");
128 	}
129 
130 	if (domain->chain != NULL) {
131 		if ((chainfile = basename(domain->chain)) != NULL) {
132 			if ((chainfile = strdup(chainfile)) == NULL)
133 				err(EXIT_FAILURE, "strdup");
134 		} else
135 			err(EXIT_FAILURE, "basename");
136 	}
137 
138 	if (domain->fullchain != NULL) {
139 		if ((fullchainfile = basename(domain->fullchain)) != NULL) {
140 			if ((fullchainfile = strdup(fullchainfile)) == NULL)
141 				err(EXIT_FAILURE, "strdup");
142 		} else
143 			err(EXIT_FAILURE, "basename");
144 	}
145 
146 	if ((auth = domain->auth) == NULL) {
147 		/* use the first authority from the config as default XXX */
148 		authority = authority_find0(conf);
149 		if (authority == NULL)
150 			errx(EXIT_FAILURE, "no authorities configured");
151 	} else {
152 		authority = authority_find(conf, auth);
153 		if (authority == NULL)
154 			errx(EXIT_FAILURE, "authority %s not found", auth);
155 	}
156 
157 	acctkey = authority->account;
158 
159 	if (acctkey == NULL) {
160 		/* XXX replace with existance check in parse.y */
161 		err(EXIT_FAILURE, "no account key in config?");
162 	}
163 
164 	if ((chngdir = domain->challengedir) == NULL)
165 		if ((chngdir = strdup(WWW_DIR)) == NULL)
166 			err(EXIT_FAILURE, "strdup");
167 
168 	/*
169 	 * Do some quick checks to see if our paths exist.
170 	 * This will be done in the children, but we might as well check
171 	 * now before the fork.
172 	 * XXX maybe use conf_check_file() from parse.y
173 	 */
174 
175 	ne = 0;
176 
177 	if (access(certdir, R_OK) == -1) {
178 		warnx("%s: cert directory must exist", certdir);
179 		ne++;
180 	}
181 
182 	if (!(popts & ACME_OPT_NEWDKEY) && access(domain->key, R_OK) == -1) {
183 		warnx("%s: domain key file must exist", domain->key);
184 		ne++;
185 	} else if ((popts & ACME_OPT_NEWDKEY) && access(domain->key, R_OK)
186 	    != -1) {
187 		dodbg("%s: domain key exists (not creating)", domain->key);
188 		popts &= ~ACME_OPT_NEWDKEY;
189 	}
190 
191 	if (access(chngdir, R_OK) == -1) {
192 		warnx("%s: challenge directory must exist", chngdir);
193 		ne++;
194 	}
195 
196 	if (!(popts & ACME_OPT_NEWACCT) && access(acctkey, R_OK) == -1) {
197 		warnx("%s: account key file must exist", acctkey);
198 		ne++;
199 	} else if ((popts & ACME_OPT_NEWACCT) && access(acctkey, R_OK) != -1) {
200 		dodbg("%s: account key exists (not creating)", acctkey);
201 		popts &= ~ACME_OPT_NEWACCT;
202 	}
203 
204 	if (ne > 0)
205 		return EXIT_FAILURE;
206 
207 	if (popts & ACME_OPT_CHECK)
208 		return EXIT_SUCCESS;
209 
210 	/* Set the zeroth altname as our domain. */
211 	altsz = domain->altname_count + 1;
212 	alts = calloc(altsz, sizeof(char *));
213 	if (alts == NULL)
214 		err(EXIT_FAILURE, "calloc");
215 	alts[0] = domain->domain;
216 	i = 1;
217 	/* XXX get rid of alts[] later */
218 	TAILQ_FOREACH(ac, &domain->altname_list, entry)
219 		alts[i++] = ac->domain;
220 
221 	/*
222 	 * Open channels between our components.
223 	 */
224 
225 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, key_fds) == -1)
226 		err(EXIT_FAILURE, "socketpair");
227 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, acct_fds) == -1)
228 		err(EXIT_FAILURE, "socketpair");
229 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, chng_fds) == -1)
230 		err(EXIT_FAILURE, "socketpair");
231 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, cert_fds) == -1)
232 		err(EXIT_FAILURE, "socketpair");
233 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, file_fds) == -1)
234 		err(EXIT_FAILURE, "socketpair");
235 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, dns_fds) == -1)
236 		err(EXIT_FAILURE, "socketpair");
237 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, rvk_fds) == -1)
238 		err(EXIT_FAILURE, "socketpair");
239 
240 	/* Start with the network-touching process. */
241 
242 	if ((pids[COMP_NET] = fork()) == -1)
243 		err(EXIT_FAILURE, "fork");
244 
245 	if (pids[COMP_NET] == 0) {
246 		proccomp = COMP_NET;
247 		close(key_fds[0]);
248 		close(acct_fds[0]);
249 		close(chng_fds[0]);
250 		close(cert_fds[0]);
251 		close(file_fds[0]);
252 		close(file_fds[1]);
253 		close(dns_fds[0]);
254 		close(rvk_fds[0]);
255 		c = netproc(key_fds[1], acct_fds[1],
256 		    chng_fds[1], cert_fds[1],
257 		    dns_fds[1], rvk_fds[1],
258 		    (popts & ACME_OPT_NEWACCT), revocate, authority,
259 		    (const char *const *)alts, altsz);
260 		free(alts);
261 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
262 	}
263 
264 	close(key_fds[1]);
265 	close(acct_fds[1]);
266 	close(chng_fds[1]);
267 	close(cert_fds[1]);
268 	close(dns_fds[1]);
269 	close(rvk_fds[1]);
270 
271 	/* Now the key-touching component. */
272 
273 	if ((pids[COMP_KEY] = fork()) == -1)
274 		err(EXIT_FAILURE, "fork");
275 
276 	if (pids[COMP_KEY] == 0) {
277 		proccomp = COMP_KEY;
278 		close(cert_fds[0]);
279 		close(dns_fds[0]);
280 		close(rvk_fds[0]);
281 		close(acct_fds[0]);
282 		close(chng_fds[0]);
283 		close(file_fds[0]);
284 		close(file_fds[1]);
285 		c = keyproc(key_fds[0], domain->key,
286 		    (const char **)alts, altsz, (popts & ACME_OPT_NEWDKEY));
287 		free(alts);
288 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
289 	}
290 
291 	close(key_fds[0]);
292 
293 	/* The account-touching component. */
294 
295 	if ((pids[COMP_ACCOUNT] = fork()) == -1)
296 		err(EXIT_FAILURE, "fork");
297 
298 	if (pids[COMP_ACCOUNT] == 0) {
299 		proccomp = COMP_ACCOUNT;
300 		free(alts);
301 		close(cert_fds[0]);
302 		close(dns_fds[0]);
303 		close(rvk_fds[0]);
304 		close(chng_fds[0]);
305 		close(file_fds[0]);
306 		close(file_fds[1]);
307 		c = acctproc(acct_fds[0], acctkey, (popts & ACME_OPT_NEWACCT));
308 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
309 	}
310 
311 	close(acct_fds[0]);
312 
313 	/* The challenge-accepting component. */
314 
315 	if ((pids[COMP_CHALLENGE] = fork()) == -1)
316 		err(EXIT_FAILURE, "fork");
317 
318 	if (pids[COMP_CHALLENGE] == 0) {
319 		proccomp = COMP_CHALLENGE;
320 		free(alts);
321 		close(cert_fds[0]);
322 		close(dns_fds[0]);
323 		close(rvk_fds[0]);
324 		close(file_fds[0]);
325 		close(file_fds[1]);
326 		c = chngproc(chng_fds[0], chngdir);
327 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
328 	}
329 
330 	close(chng_fds[0]);
331 
332 	/* The certificate-handling component. */
333 
334 	if ((pids[COMP_CERT] = fork()) == -1)
335 		err(EXIT_FAILURE, "fork");
336 
337 	if (pids[COMP_CERT] == 0) {
338 		proccomp = COMP_CERT;
339 		free(alts);
340 		close(dns_fds[0]);
341 		close(rvk_fds[0]);
342 		close(file_fds[1]);
343 		c = certproc(cert_fds[0], file_fds[0]);
344 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
345 	}
346 
347 	close(cert_fds[0]);
348 	close(file_fds[0]);
349 
350 	/* The certificate-handling component. */
351 
352 	if ((pids[COMP_FILE] = fork()) == -1)
353 		err(EXIT_FAILURE, "fork");
354 
355 	if (pids[COMP_FILE] == 0) {
356 		proccomp = COMP_FILE;
357 		free(alts);
358 		close(dns_fds[0]);
359 		close(rvk_fds[0]);
360 		c = fileproc(file_fds[1], certdir, certfile, chainfile,
361 		    fullchainfile);
362 		/*
363 		 * This is different from the other processes in that it
364 		 * can return 2 if the certificates were updated.
365 		 */
366 		exit(c > 1 ? 2 : (c ? EXIT_SUCCESS : EXIT_FAILURE));
367 	}
368 
369 	close(file_fds[1]);
370 
371 	/* The DNS lookup component. */
372 
373 	if ((pids[COMP_DNS] = fork()) == -1)
374 		err(EXIT_FAILURE, "fork");
375 
376 	if (pids[COMP_DNS] == 0) {
377 		proccomp = COMP_DNS;
378 		free(alts);
379 		close(rvk_fds[0]);
380 		c = dnsproc(dns_fds[0]);
381 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
382 	}
383 
384 	close(dns_fds[0]);
385 
386 	/* The expiration component. */
387 
388 	if ((pids[COMP_REVOKE] = fork()) == -1)
389 		err(EXIT_FAILURE, "fork");
390 
391 	if (pids[COMP_REVOKE] == 0) {
392 		proccomp = COMP_REVOKE;
393 		c = revokeproc(rvk_fds[0], certdir,
394 		    certfile != NULL ? certfile : fullchainfile,
395 		    force, revocate,
396 		    (const char *const *)alts, altsz);
397 		free(alts);
398 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
399 	}
400 
401 	close(rvk_fds[0]);
402 
403 	/* Jail: sandbox, file-system, user. */
404 
405 	if (pledge("stdio", NULL) == -1)
406 		err(EXIT_FAILURE, "pledge");
407 
408 	/*
409 	 * Collect our subprocesses.
410 	 * Require that they both have exited cleanly.
411 	 */
412 
413 	rc = checkexit(pids[COMP_KEY], COMP_KEY) +
414 	    checkexit(pids[COMP_CERT], COMP_CERT) +
415 	    checkexit(pids[COMP_NET], COMP_NET) +
416 	    checkexit_ext(&c, pids[COMP_FILE], COMP_FILE) +
417 	    checkexit(pids[COMP_ACCOUNT], COMP_ACCOUNT) +
418 	    checkexit(pids[COMP_CHALLENGE], COMP_CHALLENGE) +
419 	    checkexit(pids[COMP_DNS], COMP_DNS) +
420 	    checkexit(pids[COMP_REVOKE], COMP_REVOKE);
421 
422 	free(alts);
423 	return rc != COMP__MAX ? EXIT_FAILURE : (c == 2 ? EXIT_SUCCESS : 2);
424 usage:
425 	fprintf(stderr,
426 	    "usage: acme-client [-ADFnrv] [-f configfile] domain\n");
427 	return EXIT_FAILURE;
428 }
429