xref: /openbsd-src/usr.sbin/acme-client/revokeproc.c (revision 0b60193df276dc6f43ebb60edd076225d1cc0850)
1*0b60193dStb /*	$Id: revokeproc.c,v 1.25 2022/12/18 12:04:55 tb Exp $ */
2de579d12Sflorian /*
3de579d12Sflorian  * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv>
4de579d12Sflorian  *
5de579d12Sflorian  * Permission to use, copy, modify, and distribute this software for any
6de579d12Sflorian  * purpose with or without fee is hereby granted, provided that the above
7de579d12Sflorian  * copyright notice and this permission notice appear in all copies.
8de579d12Sflorian  *
9de579d12Sflorian  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
10de579d12Sflorian  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11de579d12Sflorian  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
12de579d12Sflorian  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13de579d12Sflorian  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14de579d12Sflorian  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15de579d12Sflorian  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16de579d12Sflorian  */
17de579d12Sflorian 
18de579d12Sflorian #include <assert.h>
19de579d12Sflorian #include <ctype.h>
20de579d12Sflorian #include <err.h>
21de579d12Sflorian #include <errno.h>
22de579d12Sflorian #include <stdio.h>
23de579d12Sflorian #include <stdlib.h>
24de579d12Sflorian #include <string.h>
25de579d12Sflorian #include <unistd.h>
26*0b60193dStb #include <vis.h>
27de579d12Sflorian 
28de579d12Sflorian #include <openssl/pem.h>
29de579d12Sflorian #include <openssl/x509.h>
30de579d12Sflorian #include <openssl/x509v3.h>
31de579d12Sflorian #include <openssl/err.h>
32de579d12Sflorian 
33de579d12Sflorian #include "extern.h"
34de579d12Sflorian 
35de579d12Sflorian #define	RENEW_ALLOW (30 * 24 * 60 * 60)
36de579d12Sflorian 
37de579d12Sflorian /*
38760a94e2Stb  * Convert the X509's expiration time into a time_t value.
39de579d12Sflorian  */
40de579d12Sflorian static time_t
X509expires(X509 * x)41de579d12Sflorian X509expires(X509 *x)
42de579d12Sflorian {
434128f05dSderaadt 	ASN1_TIME	*atim;
44de579d12Sflorian 	struct tm	 t;
45de579d12Sflorian 
46760a94e2Stb 	if ((atim = X509_getm_notAfter(x)) == NULL) {
47760a94e2Stb 		warnx("missing notAfter");
48760a94e2Stb 		return -1;
49760a94e2Stb 	}
50760a94e2Stb 
51de579d12Sflorian 	memset(&t, 0, sizeof(t));
52de579d12Sflorian 
53760a94e2Stb 	if (!ASN1_TIME_to_tm(atim, &t)) {
54de579d12Sflorian 		warnx("invalid ASN1_TIME");
55760a94e2Stb 		return -1;
56de579d12Sflorian 	}
57de579d12Sflorian 
584cf348cfStb 	return timegm(&t);
59de579d12Sflorian }
60de579d12Sflorian 
61de579d12Sflorian int
revokeproc(int fd,const char * certfile,int force,int revocate,const char * const * alts,size_t altsz)6261075b4cSflorian revokeproc(int fd, const char *certfile, int force,
6362492c74Sflorian     int revocate, const char *const *alts, size_t altsz)
64de579d12Sflorian {
650cffdb45Stb 	GENERAL_NAMES			*sans = NULL;
6661075b4cSflorian 	char				*der = NULL, *dercp, *der64 = NULL;
670cffdb45Stb 	int				 rc = 0, cc, i, len;
687bce6888Sderaadt 	size_t				*found = NULL;
697bce6888Sderaadt 	FILE				*f = NULL;
707bce6888Sderaadt 	X509				*x = NULL;
71de579d12Sflorian 	long				 lval;
72de579d12Sflorian 	enum revokeop			 op, rop;
73de579d12Sflorian 	time_t				 t;
74de579d12Sflorian 	size_t				 j;
75de579d12Sflorian 
76de579d12Sflorian 	/*
77de579d12Sflorian 	 * First try to open the certificate before we drop privileges
78de579d12Sflorian 	 * and jail ourselves.
79de579d12Sflorian 	 * We allow "f" to be NULL IFF the cert doesn't exist yet.
80de579d12Sflorian 	 */
81de579d12Sflorian 
8261075b4cSflorian 	if ((f = fopen(certfile, "r")) == NULL && errno != ENOENT) {
8361075b4cSflorian 		warn("%s", certfile);
84de579d12Sflorian 		goto out;
85de579d12Sflorian 	}
86de579d12Sflorian 
87de579d12Sflorian 	/* File-system and sandbox jailing. */
88de579d12Sflorian 
89de579d12Sflorian 	ERR_load_crypto_strings();
90de579d12Sflorian 
91ec0d8c8bSderaadt 	if (pledge("stdio", NULL) == -1) {
92ec0d8c8bSderaadt 		warn("pledge");
93de579d12Sflorian 		goto out;
94ec0d8c8bSderaadt 	}
95de579d12Sflorian 
96de579d12Sflorian 	/*
97de579d12Sflorian 	 * If we couldn't open the certificate, it doesn't exist so we
98de579d12Sflorian 	 * haven't submitted it yet, so obviously we can mark that it
99de579d12Sflorian 	 * has expired and we should renew it.
100de579d12Sflorian 	 * If we're revoking, however, then that's an error!
101de579d12Sflorian 	 * Ignore if the reader isn't reading in either case.
102de579d12Sflorian 	 */
103de579d12Sflorian 
1047cd8f039Sjsing 	if (f == NULL && revocate) {
10561075b4cSflorian 		warnx("%s: no certificate found", certfile);
106de579d12Sflorian 		(void)writeop(fd, COMM_REVOKE_RESP, REVOKE_OK);
107de579d12Sflorian 		goto out;
1087cd8f039Sjsing 	} else if (f == NULL && !revocate) {
109de579d12Sflorian 		if (writeop(fd, COMM_REVOKE_RESP, REVOKE_EXP) >= 0)
110de579d12Sflorian 			rc = 1;
111de579d12Sflorian 		goto out;
112de579d12Sflorian 	}
113de579d12Sflorian 
1147cd8f039Sjsing 	if ((x = PEM_read_X509(f, NULL, NULL, NULL)) == NULL) {
115de579d12Sflorian 		warnx("PEM_read_X509");
116de579d12Sflorian 		goto out;
117de579d12Sflorian 	}
118de579d12Sflorian 
1190cffdb45Stb 	/* Cache and sanity check X509v3 extensions. */
1200cffdb45Stb 
1210cffdb45Stb 	if (X509_check_purpose(x, -1, -1) <= 0) {
1220cffdb45Stb 		warnx("%s: invalid X509v3 extensions", certfile);
1230cffdb45Stb 		goto out;
1240cffdb45Stb 	}
1250cffdb45Stb 
126de579d12Sflorian 	/* Read out the expiration date. */
127de579d12Sflorian 
128760a94e2Stb 	if ((t = X509expires(x)) == -1) {
129de579d12Sflorian 		warnx("X509expires");
130de579d12Sflorian 		goto out;
131de579d12Sflorian 	}
132de579d12Sflorian 
1330cffdb45Stb 	/* Extract list of SAN entries from the certificate. */
134de579d12Sflorian 
1350cffdb45Stb 	sans = X509_get_ext_d2i(x, NID_subject_alt_name, NULL, NULL);
1360cffdb45Stb 	if (sans == NULL) {
13761075b4cSflorian 		warnx("%s: does not have a SAN entry", certfile);
138f0c83cb3Ssthen 		if (revocate)
139de579d12Sflorian 			goto out;
140f0c83cb3Ssthen 		force = 2;
141de579d12Sflorian 	}
142de579d12Sflorian 
143de579d12Sflorian 	/* An array of buckets: the number of entries found. */
144de579d12Sflorian 
1457cd8f039Sjsing 	if ((found = calloc(altsz, sizeof(size_t))) == NULL) {
146de579d12Sflorian 		warn("calloc");
147de579d12Sflorian 		goto out;
148de579d12Sflorian 	}
149de579d12Sflorian 
150de579d12Sflorian 	/*
1510cffdb45Stb 	 * Ensure the certificate's SAN entries fully cover those from the
1520cffdb45Stb 	 * configuration file and that all domains are represented only once.
153de579d12Sflorian 	 */
154de579d12Sflorian 
1550cffdb45Stb 	for (i = 0; i < sk_GENERAL_NAME_num(sans); i++) {
1560cffdb45Stb 		GENERAL_NAME		*gen_name;
1570cffdb45Stb 		const ASN1_IA5STRING	*name;
1580cffdb45Stb 		const unsigned char	*name_buf;
1590cffdb45Stb 		int			 name_len;
1600cffdb45Stb 		int			 name_type;
1610cffdb45Stb 
1620cffdb45Stb 		gen_name = sk_GENERAL_NAME_value(sans, i);
1630cffdb45Stb 		assert(gen_name != NULL);
1640cffdb45Stb 
1650cffdb45Stb 		name = GENERAL_NAME_get0_value(gen_name, &name_type);
1660cffdb45Stb 		if (name_type != GEN_DNS)
167de579d12Sflorian 			continue;
1680cffdb45Stb 
1690cffdb45Stb 		/* name_buf isn't a C string and could contain embedded NULs. */
1700cffdb45Stb 		name_buf = ASN1_STRING_get0_data(name);
1710cffdb45Stb 		name_len = ASN1_STRING_length(name);
1720cffdb45Stb 
1730cffdb45Stb 		for (j = 0; j < altsz; j++) {
1740cffdb45Stb 			if ((size_t)name_len != strlen(alts[j]))
175de579d12Sflorian 				continue;
1760cffdb45Stb 			if (memcmp(name_buf, alts[j], name_len) == 0)
177de579d12Sflorian 				break;
1780cffdb45Stb 		}
179de579d12Sflorian 		if (j == altsz) {
180f0c83cb3Ssthen 			if (revocate) {
181*0b60193dStb 				char *visbuf;
182*0b60193dStb 
183*0b60193dStb 				visbuf = calloc(4, name_len + 1);
184*0b60193dStb 				if (visbuf == NULL) {
185*0b60193dStb 					warn("%s: unexpected SAN", certfile);
186*0b60193dStb 					goto out;
187*0b60193dStb 				}
188*0b60193dStb 				strvisx(visbuf, name_buf, name_len, VIS_SAFE);
189*0b60193dStb 				warnx("%s: unexpected SAN entry: %s",
190*0b60193dStb 				    certfile, visbuf);
191*0b60193dStb 				free(visbuf);
192de579d12Sflorian 				goto out;
193de579d12Sflorian 			}
194f0c83cb3Ssthen 			force = 2;
19576a7f400Sotto 			continue;
196f0c83cb3Ssthen 		}
197de579d12Sflorian 		if (found[j]++) {
198f0c83cb3Ssthen 			if (revocate) {
1990cffdb45Stb 				warnx("%s: duplicate SAN entry: %.*s",
2000cffdb45Stb 				    certfile, name_len, name_buf);
201de579d12Sflorian 				goto out;
202de579d12Sflorian 			}
203f0c83cb3Ssthen 			force = 2;
204f0c83cb3Ssthen 		}
205de579d12Sflorian 	}
206de579d12Sflorian 
207f0c83cb3Ssthen 	for (j = 0; j < altsz; j++) {
208de579d12Sflorian 		if (found[j])
209de579d12Sflorian 			continue;
210f0c83cb3Ssthen 		if (revocate) {
21161075b4cSflorian 			warnx("%s: domain not listed: %s", certfile, alts[j]);
212de579d12Sflorian 			goto out;
213de579d12Sflorian 		}
214f0c83cb3Ssthen 		force = 2;
215f0c83cb3Ssthen 	}
216de579d12Sflorian 
217de579d12Sflorian 	/*
218de579d12Sflorian 	 * If we're going to revoke, write the certificate to the
219de579d12Sflorian 	 * netproc in DER and base64-encoded format.
220de579d12Sflorian 	 * Then exit: we have nothing left to do.
221de579d12Sflorian 	 */
222de579d12Sflorian 
223ee27a5e1Sderaadt 	if (revocate) {
22461075b4cSflorian 		dodbg("%s: revocation", certfile);
225de579d12Sflorian 
226de579d12Sflorian 		/*
227de579d12Sflorian 		 * First, tell netproc we're online.
228de579d12Sflorian 		 * If they're down, then just exit without warning.
229de579d12Sflorian 		 */
230de579d12Sflorian 
231de579d12Sflorian 		cc = writeop(fd, COMM_REVOKE_RESP, REVOKE_EXP);
2327cd8f039Sjsing 		if (cc == 0)
233de579d12Sflorian 			rc = 1;
234de579d12Sflorian 		if (cc <= 0)
235de579d12Sflorian 			goto out;
236de579d12Sflorian 
237de579d12Sflorian 		if ((len = i2d_X509(x, NULL)) < 0) {
238de579d12Sflorian 			warnx("i2d_X509");
239de579d12Sflorian 			goto out;
2407cd8f039Sjsing 		} else if ((der = dercp = malloc(len)) == NULL) {
241de579d12Sflorian 			warn("malloc");
242de579d12Sflorian 			goto out;
243de579d12Sflorian 		} else if (len != i2d_X509(x, (u_char **)&dercp)) {
244de579d12Sflorian 			warnx("i2d_X509");
245de579d12Sflorian 			goto out;
2467cd8f039Sjsing 		} else if ((der64 = base64buf_url(der, len)) == NULL) {
247de579d12Sflorian 			warnx("base64buf_url");
248de579d12Sflorian 			goto out;
249de579d12Sflorian 		} else if (writestr(fd, COMM_CSR, der64) >= 0)
250de579d12Sflorian 			rc = 1;
251de579d12Sflorian 
252de579d12Sflorian 		goto out;
253de579d12Sflorian 	}
254de579d12Sflorian 
255de579d12Sflorian 	rop = time(NULL) >= (t - RENEW_ALLOW) ? REVOKE_EXP : REVOKE_OK;
256de579d12Sflorian 
2574de82fa3Sderaadt 	if (rop == REVOKE_EXP)
25861075b4cSflorian 		dodbg("%s: certificate renewable: %lld days left",
25961075b4cSflorian 		    certfile, (long long)(t - time(NULL)) / 24 / 60 / 60);
260de579d12Sflorian 	else
26161075b4cSflorian 		dodbg("%s: certificate valid: %lld days left",
26261075b4cSflorian 		    certfile, (long long)(t - time(NULL)) / 24 / 60 / 60);
263de579d12Sflorian 
2644de82fa3Sderaadt 	if (rop == REVOKE_OK && force) {
265f0c83cb3Ssthen 		warnx("%s: %sforcing renewal", certfile,
266f0c83cb3Ssthen 		    force == 2 ? "domain list changed, " : "");
267de579d12Sflorian 		rop = REVOKE_EXP;
268de579d12Sflorian 	}
269de579d12Sflorian 
270de579d12Sflorian 	/*
271de579d12Sflorian 	 * We can re-submit it given RENEW_ALLOW time before.
272de579d12Sflorian 	 * If netproc is down, just exit.
273de579d12Sflorian 	 */
274de579d12Sflorian 
2757cd8f039Sjsing 	if ((cc = writeop(fd, COMM_REVOKE_RESP, rop)) == 0)
276de579d12Sflorian 		rc = 1;
277de579d12Sflorian 	if (cc <= 0)
278de579d12Sflorian 		goto out;
279de579d12Sflorian 
280de579d12Sflorian 	op = REVOKE__MAX;
2817cd8f039Sjsing 	if ((lval = readop(fd, COMM_REVOKE_OP)) == 0)
282de579d12Sflorian 		op = REVOKE_STOP;
2837cd8f039Sjsing 	else if (lval == REVOKE_CHECK)
284de579d12Sflorian 		op = lval;
285de579d12Sflorian 
2864de82fa3Sderaadt 	if (op == REVOKE__MAX) {
287de579d12Sflorian 		warnx("unknown operation from netproc");
288de579d12Sflorian 		goto out;
2894de82fa3Sderaadt 	} else if (op == REVOKE_STOP) {
290de579d12Sflorian 		rc = 1;
291de579d12Sflorian 		goto out;
292de579d12Sflorian 	}
293de579d12Sflorian 
294de579d12Sflorian 	rc = 1;
295de579d12Sflorian out:
296de579d12Sflorian 	close(fd);
2977cd8f039Sjsing 	if (f != NULL)
298de579d12Sflorian 		fclose(f);
299de579d12Sflorian 	X509_free(x);
3000cffdb45Stb 	GENERAL_NAMES_free(sans);
301de579d12Sflorian 	free(der);
302de579d12Sflorian 	free(found);
303de579d12Sflorian 	free(der64);
304de579d12Sflorian 	ERR_print_errors_fp(stderr);
305de579d12Sflorian 	ERR_free_strings();
30634335c11Sjsing 	return rc;
307de579d12Sflorian }
308