1 /* $Id: revokeproc.c,v 1.25 2022/12/18 12:04:55 tb 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 <assert.h>
19 #include <ctype.h>
20 #include <err.h>
21 #include <errno.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <unistd.h>
26 #include <vis.h>
27
28 #include <openssl/pem.h>
29 #include <openssl/x509.h>
30 #include <openssl/x509v3.h>
31 #include <openssl/err.h>
32
33 #include "extern.h"
34
35 #define RENEW_ALLOW (30 * 24 * 60 * 60)
36
37 /*
38 * Convert the X509's expiration time into a time_t value.
39 */
40 static time_t
X509expires(X509 * x)41 X509expires(X509 *x)
42 {
43 ASN1_TIME *atim;
44 struct tm t;
45
46 if ((atim = X509_getm_notAfter(x)) == NULL) {
47 warnx("missing notAfter");
48 return -1;
49 }
50
51 memset(&t, 0, sizeof(t));
52
53 if (!ASN1_TIME_to_tm(atim, &t)) {
54 warnx("invalid ASN1_TIME");
55 return -1;
56 }
57
58 return timegm(&t);
59 }
60
61 int
revokeproc(int fd,const char * certfile,int force,int revocate,const char * const * alts,size_t altsz)62 revokeproc(int fd, const char *certfile, int force,
63 int revocate, const char *const *alts, size_t altsz)
64 {
65 GENERAL_NAMES *sans = NULL;
66 char *der = NULL, *dercp, *der64 = NULL;
67 int rc = 0, cc, i, len;
68 size_t *found = NULL;
69 FILE *f = NULL;
70 X509 *x = NULL;
71 long lval;
72 enum revokeop op, rop;
73 time_t t;
74 size_t j;
75
76 /*
77 * First try to open the certificate before we drop privileges
78 * and jail ourselves.
79 * We allow "f" to be NULL IFF the cert doesn't exist yet.
80 */
81
82 if ((f = fopen(certfile, "r")) == NULL && errno != ENOENT) {
83 warn("%s", certfile);
84 goto out;
85 }
86
87 /* File-system and sandbox jailing. */
88
89 ERR_load_crypto_strings();
90
91 if (pledge("stdio", NULL) == -1) {
92 warn("pledge");
93 goto out;
94 }
95
96 /*
97 * If we couldn't open the certificate, it doesn't exist so we
98 * haven't submitted it yet, so obviously we can mark that it
99 * has expired and we should renew it.
100 * If we're revoking, however, then that's an error!
101 * Ignore if the reader isn't reading in either case.
102 */
103
104 if (f == NULL && revocate) {
105 warnx("%s: no certificate found", certfile);
106 (void)writeop(fd, COMM_REVOKE_RESP, REVOKE_OK);
107 goto out;
108 } else if (f == NULL && !revocate) {
109 if (writeop(fd, COMM_REVOKE_RESP, REVOKE_EXP) >= 0)
110 rc = 1;
111 goto out;
112 }
113
114 if ((x = PEM_read_X509(f, NULL, NULL, NULL)) == NULL) {
115 warnx("PEM_read_X509");
116 goto out;
117 }
118
119 /* Cache and sanity check X509v3 extensions. */
120
121 if (X509_check_purpose(x, -1, -1) <= 0) {
122 warnx("%s: invalid X509v3 extensions", certfile);
123 goto out;
124 }
125
126 /* Read out the expiration date. */
127
128 if ((t = X509expires(x)) == -1) {
129 warnx("X509expires");
130 goto out;
131 }
132
133 /* Extract list of SAN entries from the certificate. */
134
135 sans = X509_get_ext_d2i(x, NID_subject_alt_name, NULL, NULL);
136 if (sans == NULL) {
137 warnx("%s: does not have a SAN entry", certfile);
138 if (revocate)
139 goto out;
140 force = 2;
141 }
142
143 /* An array of buckets: the number of entries found. */
144
145 if ((found = calloc(altsz, sizeof(size_t))) == NULL) {
146 warn("calloc");
147 goto out;
148 }
149
150 /*
151 * Ensure the certificate's SAN entries fully cover those from the
152 * configuration file and that all domains are represented only once.
153 */
154
155 for (i = 0; i < sk_GENERAL_NAME_num(sans); i++) {
156 GENERAL_NAME *gen_name;
157 const ASN1_IA5STRING *name;
158 const unsigned char *name_buf;
159 int name_len;
160 int name_type;
161
162 gen_name = sk_GENERAL_NAME_value(sans, i);
163 assert(gen_name != NULL);
164
165 name = GENERAL_NAME_get0_value(gen_name, &name_type);
166 if (name_type != GEN_DNS)
167 continue;
168
169 /* name_buf isn't a C string and could contain embedded NULs. */
170 name_buf = ASN1_STRING_get0_data(name);
171 name_len = ASN1_STRING_length(name);
172
173 for (j = 0; j < altsz; j++) {
174 if ((size_t)name_len != strlen(alts[j]))
175 continue;
176 if (memcmp(name_buf, alts[j], name_len) == 0)
177 break;
178 }
179 if (j == altsz) {
180 if (revocate) {
181 char *visbuf;
182
183 visbuf = calloc(4, name_len + 1);
184 if (visbuf == NULL) {
185 warn("%s: unexpected SAN", certfile);
186 goto out;
187 }
188 strvisx(visbuf, name_buf, name_len, VIS_SAFE);
189 warnx("%s: unexpected SAN entry: %s",
190 certfile, visbuf);
191 free(visbuf);
192 goto out;
193 }
194 force = 2;
195 continue;
196 }
197 if (found[j]++) {
198 if (revocate) {
199 warnx("%s: duplicate SAN entry: %.*s",
200 certfile, name_len, name_buf);
201 goto out;
202 }
203 force = 2;
204 }
205 }
206
207 for (j = 0; j < altsz; j++) {
208 if (found[j])
209 continue;
210 if (revocate) {
211 warnx("%s: domain not listed: %s", certfile, alts[j]);
212 goto out;
213 }
214 force = 2;
215 }
216
217 /*
218 * If we're going to revoke, write the certificate to the
219 * netproc in DER and base64-encoded format.
220 * Then exit: we have nothing left to do.
221 */
222
223 if (revocate) {
224 dodbg("%s: revocation", certfile);
225
226 /*
227 * First, tell netproc we're online.
228 * If they're down, then just exit without warning.
229 */
230
231 cc = writeop(fd, COMM_REVOKE_RESP, REVOKE_EXP);
232 if (cc == 0)
233 rc = 1;
234 if (cc <= 0)
235 goto out;
236
237 if ((len = i2d_X509(x, NULL)) < 0) {
238 warnx("i2d_X509");
239 goto out;
240 } else if ((der = dercp = malloc(len)) == NULL) {
241 warn("malloc");
242 goto out;
243 } else if (len != i2d_X509(x, (u_char **)&dercp)) {
244 warnx("i2d_X509");
245 goto out;
246 } else if ((der64 = base64buf_url(der, len)) == NULL) {
247 warnx("base64buf_url");
248 goto out;
249 } else if (writestr(fd, COMM_CSR, der64) >= 0)
250 rc = 1;
251
252 goto out;
253 }
254
255 rop = time(NULL) >= (t - RENEW_ALLOW) ? REVOKE_EXP : REVOKE_OK;
256
257 if (rop == REVOKE_EXP)
258 dodbg("%s: certificate renewable: %lld days left",
259 certfile, (long long)(t - time(NULL)) / 24 / 60 / 60);
260 else
261 dodbg("%s: certificate valid: %lld days left",
262 certfile, (long long)(t - time(NULL)) / 24 / 60 / 60);
263
264 if (rop == REVOKE_OK && force) {
265 warnx("%s: %sforcing renewal", certfile,
266 force == 2 ? "domain list changed, " : "");
267 rop = REVOKE_EXP;
268 }
269
270 /*
271 * We can re-submit it given RENEW_ALLOW time before.
272 * If netproc is down, just exit.
273 */
274
275 if ((cc = writeop(fd, COMM_REVOKE_RESP, rop)) == 0)
276 rc = 1;
277 if (cc <= 0)
278 goto out;
279
280 op = REVOKE__MAX;
281 if ((lval = readop(fd, COMM_REVOKE_OP)) == 0)
282 op = REVOKE_STOP;
283 else if (lval == REVOKE_CHECK)
284 op = lval;
285
286 if (op == REVOKE__MAX) {
287 warnx("unknown operation from netproc");
288 goto out;
289 } else if (op == REVOKE_STOP) {
290 rc = 1;
291 goto out;
292 }
293
294 rc = 1;
295 out:
296 close(fd);
297 if (f != NULL)
298 fclose(f);
299 X509_free(x);
300 GENERAL_NAMES_free(sans);
301 free(der);
302 free(found);
303 free(der64);
304 ERR_print_errors_fp(stderr);
305 ERR_free_strings();
306 return rc;
307 }
308