xref: /openbsd-src/usr.sbin/acme-client/main.c (revision 897fc685943471cf985a0fe38ba076ea6fe74fa5)
1 /*	$Id: main.c,v 1.36 2017/11/27 01:58:52 florian 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, "FADrvnf:")) != -1)
60 		switch (c) {
61 		case 'f':
62 			if ((conffile = strdup(optarg)) == NULL)
63 				err(EXIT_FAILURE, "strdup");
64 			break;
65 		case 'F':
66 			force = 1;
67 			break;
68 		case 'A':
69 			popts |= ACME_OPT_NEWACCT;
70 			break;
71 		case 'D':
72 			popts |= ACME_OPT_NEWDKEY;
73 			break;
74 		case 'r':
75 			revocate = 1;
76 			break;
77 		case 'v':
78 			verbose = verbose ? 2 : 1;
79 			popts |= ACME_OPT_VERBOSE;
80 			break;
81 		case 'n':
82 			popts |= ACME_OPT_CHECK;
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 		exit(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 	if (domain->challengedir == NULL)
164 		chngdir = strdup(WWW_DIR);
165 	else
166 		chngdir = domain->challengedir;
167 
168 	if (chngdir == NULL)
169 		err(EXIT_FAILURE, "strdup");
170 
171 	/*
172 	 * Do some quick checks to see if our paths exist.
173 	 * This will be done in the children, but we might as well check
174 	 * now before the fork.
175 	 * XXX maybe use conf_check_file() from parse.y
176 	 */
177 
178 	ne = 0;
179 
180 	if (access(certdir, R_OK) == -1) {
181 		warnx("%s: cert directory must exist", certdir);
182 		ne++;
183 	}
184 
185 	if (!(popts & ACME_OPT_NEWDKEY) && access(domain->key, R_OK) == -1) {
186 		warnx("%s: domain key file must exist", domain->key);
187 		ne++;
188 	} else if ((popts & ACME_OPT_NEWDKEY) && access(domain->key, R_OK) != -1) {
189 		dodbg("%s: domain key exists (not creating)", domain->key);
190 		popts &= ~ACME_OPT_NEWDKEY;
191 	}
192 
193 	if (access(chngdir, R_OK) == -1) {
194 		warnx("%s: challenge directory must exist", chngdir);
195 		ne++;
196 	}
197 
198 	if (!(popts & ACME_OPT_NEWACCT) && access(acctkey, R_OK) == -1) {
199 		warnx("%s: account key file must exist", acctkey);
200 		ne++;
201 	} else if ((popts & ACME_OPT_NEWACCT) && access(acctkey, R_OK) != -1) {
202 		dodbg("%s: account key exists (not creating)", acctkey);
203 		popts &= ~ACME_OPT_NEWACCT;
204 	}
205 
206 	if (ne > 0)
207 		exit(EXIT_FAILURE);
208 
209 	if (popts & ACME_OPT_CHECK)
210 		exit(EXIT_SUCCESS);
211 
212 	/* Set the zeroth altname as our domain. */
213 	altsz = domain->altname_count + 1;
214 	alts = calloc(altsz, sizeof(char *));
215 	if (alts == NULL)
216 		err(EXIT_FAILURE, "calloc");
217 	alts[0] = domain->domain;
218 	i = 1;
219 	/* XXX get rid of alts[] later */
220 	TAILQ_FOREACH(ac, &domain->altname_list, entry)
221 		alts[i++] = ac->domain;
222 
223 	/*
224 	 * Open channels between our components.
225 	 */
226 
227 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, key_fds) == -1)
228 		err(EXIT_FAILURE, "socketpair");
229 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, acct_fds) == -1)
230 		err(EXIT_FAILURE, "socketpair");
231 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, chng_fds) == -1)
232 		err(EXIT_FAILURE, "socketpair");
233 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, cert_fds) == -1)
234 		err(EXIT_FAILURE, "socketpair");
235 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, file_fds) == -1)
236 		err(EXIT_FAILURE, "socketpair");
237 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, dns_fds) == -1)
238 		err(EXIT_FAILURE, "socketpair");
239 	if (socketpair(AF_UNIX, SOCK_STREAM, 0, rvk_fds) == -1)
240 		err(EXIT_FAILURE, "socketpair");
241 
242 	/* Start with the network-touching process. */
243 
244 	if ((pids[COMP_NET] = fork()) == -1)
245 		err(EXIT_FAILURE, "fork");
246 
247 	if (pids[COMP_NET] == 0) {
248 		proccomp = COMP_NET;
249 		close(key_fds[0]);
250 		close(acct_fds[0]);
251 		close(chng_fds[0]);
252 		close(cert_fds[0]);
253 		close(file_fds[0]);
254 		close(file_fds[1]);
255 		close(dns_fds[0]);
256 		close(rvk_fds[0]);
257 		c = netproc(key_fds[1], acct_fds[1],
258 		    chng_fds[1], cert_fds[1],
259 		    dns_fds[1], rvk_fds[1],
260 		    (popts & ACME_OPT_NEWACCT), revocate, authority,
261 		    (const char *const *)alts, altsz);
262 		free(alts);
263 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
264 	}
265 
266 	close(key_fds[1]);
267 	close(acct_fds[1]);
268 	close(chng_fds[1]);
269 	close(cert_fds[1]);
270 	close(dns_fds[1]);
271 	close(rvk_fds[1]);
272 
273 	/* Now the key-touching component. */
274 
275 	if ((pids[COMP_KEY] = fork()) == -1)
276 		err(EXIT_FAILURE, "fork");
277 
278 	if (pids[COMP_KEY] == 0) {
279 		proccomp = COMP_KEY;
280 		close(cert_fds[0]);
281 		close(dns_fds[0]);
282 		close(rvk_fds[0]);
283 		close(acct_fds[0]);
284 		close(chng_fds[0]);
285 		close(file_fds[0]);
286 		close(file_fds[1]);
287 		c = keyproc(key_fds[0], domain->key,
288 		    (const char **)alts, altsz, (popts & ACME_OPT_NEWDKEY));
289 		free(alts);
290 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
291 	}
292 
293 	close(key_fds[0]);
294 
295 	/* The account-touching component. */
296 
297 	if ((pids[COMP_ACCOUNT] = fork()) == -1)
298 		err(EXIT_FAILURE, "fork");
299 
300 	if (pids[COMP_ACCOUNT] == 0) {
301 		proccomp = COMP_ACCOUNT;
302 		free(alts);
303 		close(cert_fds[0]);
304 		close(dns_fds[0]);
305 		close(rvk_fds[0]);
306 		close(chng_fds[0]);
307 		close(file_fds[0]);
308 		close(file_fds[1]);
309 		c = acctproc(acct_fds[0], acctkey, (popts & ACME_OPT_NEWACCT));
310 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
311 	}
312 
313 	close(acct_fds[0]);
314 
315 	/* The challenge-accepting component. */
316 
317 	if ((pids[COMP_CHALLENGE] = fork()) == -1)
318 		err(EXIT_FAILURE, "fork");
319 
320 	if (pids[COMP_CHALLENGE] == 0) {
321 		proccomp = COMP_CHALLENGE;
322 		free(alts);
323 		close(cert_fds[0]);
324 		close(dns_fds[0]);
325 		close(rvk_fds[0]);
326 		close(file_fds[0]);
327 		close(file_fds[1]);
328 		c = chngproc(chng_fds[0], chngdir);
329 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
330 	}
331 
332 	close(chng_fds[0]);
333 
334 	/* The certificate-handling component. */
335 
336 	if ((pids[COMP_CERT] = fork()) == -1)
337 		err(EXIT_FAILURE, "fork");
338 
339 	if (pids[COMP_CERT] == 0) {
340 		proccomp = COMP_CERT;
341 		free(alts);
342 		close(dns_fds[0]);
343 		close(rvk_fds[0]);
344 		close(file_fds[1]);
345 		c = certproc(cert_fds[0], file_fds[0]);
346 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
347 	}
348 
349 	close(cert_fds[0]);
350 	close(file_fds[0]);
351 
352 	/* The certificate-handling component. */
353 
354 	if ((pids[COMP_FILE] = fork()) == -1)
355 		err(EXIT_FAILURE, "fork");
356 
357 	if (pids[COMP_FILE] == 0) {
358 		proccomp = COMP_FILE;
359 		free(alts);
360 		close(dns_fds[0]);
361 		close(rvk_fds[0]);
362 		c = fileproc(file_fds[1], certdir, certfile, chainfile,
363 		    fullchainfile);
364 		/*
365 		 * This is different from the other processes in that it
366 		 * can return 2 if the certificates were updated.
367 		 */
368 		exit(c > 1 ? 2 : (c ? EXIT_SUCCESS : EXIT_FAILURE));
369 	}
370 
371 	close(file_fds[1]);
372 
373 	/* The DNS lookup component. */
374 
375 	if ((pids[COMP_DNS] = fork()) == -1)
376 		err(EXIT_FAILURE, "fork");
377 
378 	if (pids[COMP_DNS] == 0) {
379 		proccomp = COMP_DNS;
380 		free(alts);
381 		close(rvk_fds[0]);
382 		c = dnsproc(dns_fds[0]);
383 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
384 	}
385 
386 	close(dns_fds[0]);
387 
388 	/* The expiration component. */
389 
390 	if ((pids[COMP_REVOKE] = fork()) == -1)
391 		err(EXIT_FAILURE, "fork");
392 
393 	if (pids[COMP_REVOKE] == 0) {
394 		proccomp = COMP_REVOKE;
395 		c = revokeproc(rvk_fds[0], certdir,
396 		    certfile != NULL ? certfile : fullchainfile,
397 		    force, revocate,
398 		    (const char *const *)alts, altsz);
399 		free(alts);
400 		exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
401 	}
402 
403 	close(rvk_fds[0]);
404 
405 	/* Jail: sandbox, file-system, user. */
406 
407 	if (pledge("stdio", NULL) == -1) {
408 		warn("pledge");
409 		exit(EXIT_FAILURE);
410 	}
411 
412 	/*
413 	 * Collect our subprocesses.
414 	 * Require that they both have exited cleanly.
415 	 */
416 
417 	rc = checkexit(pids[COMP_KEY], COMP_KEY) +
418 	    checkexit(pids[COMP_CERT], COMP_CERT) +
419 	    checkexit(pids[COMP_NET], COMP_NET) +
420 	    checkexit_ext(&c, pids[COMP_FILE], COMP_FILE) +
421 	    checkexit(pids[COMP_ACCOUNT], COMP_ACCOUNT) +
422 	    checkexit(pids[COMP_CHALLENGE], COMP_CHALLENGE) +
423 	    checkexit(pids[COMP_DNS], COMP_DNS) +
424 	    checkexit(pids[COMP_REVOKE], COMP_REVOKE);
425 
426 	free(alts);
427 	return rc != COMP__MAX ? EXIT_FAILURE : (c == 2 ? EXIT_SUCCESS : 2);
428 usage:
429 	fprintf(stderr,
430 	    "usage: acme-client [-ADFnrv] [-f configfile] domain\n");
431 	return EXIT_FAILURE;
432 }
433