1 |
|
|
/* $Id: revokeproc.c,v 1.13 2017/07/08 13:37:23 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 |
|
|
|
27 |
|
|
#include <openssl/pem.h> |
28 |
|
|
#include <openssl/x509.h> |
29 |
|
|
#include <openssl/x509v3.h> |
30 |
|
|
#include <openssl/err.h> |
31 |
|
|
|
32 |
|
|
#include "extern.h" |
33 |
|
|
|
34 |
|
|
#define RENEW_ALLOW (30 * 24 * 60 * 60) |
35 |
|
|
|
36 |
|
|
/* |
37 |
|
|
* Convert the X509's expiration time (which is in ASN1_TIME format) |
38 |
|
|
* into a time_t value. |
39 |
|
|
* There are lots of suggestions on the Internet on how to do this and |
40 |
|
|
* they're really, really unsafe. |
41 |
|
|
* Adapt those poor solutions to a safe one. |
42 |
|
|
*/ |
43 |
|
|
static time_t |
44 |
|
|
X509expires(X509 *x) |
45 |
|
|
{ |
46 |
|
|
ASN1_TIME *atim; |
47 |
|
|
struct tm t; |
48 |
|
|
unsigned char *str; |
49 |
|
|
size_t i = 0; |
50 |
|
|
|
51 |
|
|
atim = X509_get_notAfter(x); |
52 |
|
|
str = atim->data; |
53 |
|
|
memset(&t, 0, sizeof(t)); |
54 |
|
|
|
55 |
|
|
/* Account for 2 and 4-digit time. */ |
56 |
|
|
|
57 |
|
|
if (atim->type == V_ASN1_UTCTIME) { |
58 |
|
|
if (atim->length <= 2) { |
59 |
|
|
warnx("invalid ASN1_TIME"); |
60 |
|
|
return (time_t)-1; |
61 |
|
|
} |
62 |
|
|
t.tm_year = (str[0] - '0') * 10 + (str[1] - '0'); |
63 |
|
|
if (t.tm_year < 70) |
64 |
|
|
t.tm_year += 100; |
65 |
|
|
i = 2; |
66 |
|
|
} else if (atim->type == V_ASN1_GENERALIZEDTIME) { |
67 |
|
|
if (atim->length <= 4) { |
68 |
|
|
warnx("invalid ASN1_TIME"); |
69 |
|
|
return (time_t)-1; |
70 |
|
|
} |
71 |
|
|
t.tm_year = (str[0] - '0') * 1000 + (str[1] - '0') * 100 + |
72 |
|
|
(str[2] - '0') * 10 + (str[3] - '0'); |
73 |
|
|
t.tm_year -= 1900; |
74 |
|
|
i = 4; |
75 |
|
|
} |
76 |
|
|
|
77 |
|
|
/* Now the post-year parts. */ |
78 |
|
|
|
79 |
|
|
if (atim->length <= (int)i + 10) { |
80 |
|
|
warnx("invalid ASN1_TIME"); |
81 |
|
|
return (time_t)-1; |
82 |
|
|
} |
83 |
|
|
|
84 |
|
|
t.tm_mon = ((str[i + 0] - '0') * 10 + (str[i + 1] - '0')) - 1; |
85 |
|
|
t.tm_mday = (str[i + 2] - '0') * 10 + (str[i + 3] - '0'); |
86 |
|
|
t.tm_hour = (str[i + 4] - '0') * 10 + (str[i + 5] - '0'); |
87 |
|
|
t.tm_min = (str[i + 6] - '0') * 10 + (str[i + 7] - '0'); |
88 |
|
|
t.tm_sec = (str[i + 8] - '0') * 10 + (str[i + 9] - '0'); |
89 |
|
|
|
90 |
|
|
return mktime(&t); |
91 |
|
|
} |
92 |
|
|
|
93 |
|
|
int |
94 |
|
|
revokeproc(int fd, const char *certdir, const char *certfile, int force, |
95 |
|
|
int revocate, const char *const *alts, size_t altsz) |
96 |
|
|
{ |
97 |
|
|
char *path = NULL, *der = NULL, *dercp, *der64 = NULL; |
98 |
|
|
char *san = NULL, *str, *tok; |
99 |
|
|
int rc = 0, cc, i, extsz, ssz, len; |
100 |
|
|
size_t *found = NULL; |
101 |
|
|
BIO *bio = NULL; |
102 |
|
|
FILE *f = NULL; |
103 |
|
|
X509 *x = NULL; |
104 |
|
|
long lval; |
105 |
|
|
enum revokeop op, rop; |
106 |
|
|
time_t t; |
107 |
|
|
X509_EXTENSION *ex; |
108 |
|
|
ASN1_OBJECT *obj; |
109 |
|
|
size_t j; |
110 |
|
|
|
111 |
|
|
/* |
112 |
|
|
* First try to open the certificate before we drop privileges |
113 |
|
|
* and jail ourselves. |
114 |
|
|
* We allow "f" to be NULL IFF the cert doesn't exist yet. |
115 |
|
|
*/ |
116 |
|
|
|
117 |
|
|
if (asprintf(&path, "%s/%s", certdir, certfile) == -1) { |
118 |
|
|
warn("asprintf"); |
119 |
|
|
goto out; |
120 |
|
|
} else if ((f = fopen(path, "r")) == NULL && errno != ENOENT) { |
121 |
|
|
warn("%s", path); |
122 |
|
|
goto out; |
123 |
|
|
} |
124 |
|
|
|
125 |
|
|
/* File-system and sandbox jailing. */ |
126 |
|
|
|
127 |
|
|
ERR_load_crypto_strings(); |
128 |
|
|
|
129 |
|
|
if (pledge("stdio flock rpath cpath wpath", NULL) == -1) { |
130 |
|
|
warn("pledge"); |
131 |
|
|
goto out; |
132 |
|
|
} |
133 |
|
|
|
134 |
|
|
/* |
135 |
|
|
* If we couldn't open the certificate, it doesn't exist so we |
136 |
|
|
* haven't submitted it yet, so obviously we can mark that it |
137 |
|
|
* has expired and we should renew it. |
138 |
|
|
* If we're revoking, however, then that's an error! |
139 |
|
|
* Ignore if the reader isn't reading in either case. |
140 |
|
|
*/ |
141 |
|
|
|
142 |
|
|
if (f == NULL && revocate) { |
143 |
|
|
warnx("%s/%s: no certificate found", certdir, certfile); |
144 |
|
|
(void)writeop(fd, COMM_REVOKE_RESP, REVOKE_OK); |
145 |
|
|
goto out; |
146 |
|
|
} else if (f == NULL && !revocate) { |
147 |
|
|
if (writeop(fd, COMM_REVOKE_RESP, REVOKE_EXP) >= 0) |
148 |
|
|
rc = 1; |
149 |
|
|
goto out; |
150 |
|
|
} |
151 |
|
|
|
152 |
|
|
if ((x = PEM_read_X509(f, NULL, NULL, NULL)) == NULL) { |
153 |
|
|
warnx("PEM_read_X509"); |
154 |
|
|
goto out; |
155 |
|
|
} |
156 |
|
|
|
157 |
|
|
/* Read out the expiration date. */ |
158 |
|
|
|
159 |
|
|
if ((t = X509expires(x)) == (time_t)-1) { |
160 |
|
|
warnx("X509expires"); |
161 |
|
|
goto out; |
162 |
|
|
} |
163 |
|
|
|
164 |
|
|
/* |
165 |
|
|
* Next, the long process to make sure that the SAN entries |
166 |
|
|
* listed with the certificate fully cover those passed on the |
167 |
|
|
* command line. |
168 |
|
|
*/ |
169 |
|
|
|
170 |
|
|
extsz = x->cert_info->extensions != NULL ? |
171 |
|
|
sk_X509_EXTENSION_num(x->cert_info->extensions) : 0; |
172 |
|
|
|
173 |
|
|
/* Scan til we find the SAN NID. */ |
174 |
|
|
|
175 |
|
|
for (i = 0; i < extsz; i++) { |
176 |
|
|
ex = sk_X509_EXTENSION_value(x->cert_info->extensions, i); |
177 |
|
|
assert(ex != NULL); |
178 |
|
|
obj = X509_EXTENSION_get_object(ex); |
179 |
|
|
assert(obj != NULL); |
180 |
|
|
if (NID_subject_alt_name != OBJ_obj2nid(obj)) |
181 |
|
|
continue; |
182 |
|
|
|
183 |
|
|
if (san != NULL) { |
184 |
|
|
warnx("%s/%s: two SAN entries", certdir, certfile); |
185 |
|
|
goto out; |
186 |
|
|
} |
187 |
|
|
|
188 |
|
|
bio = BIO_new(BIO_s_mem()); |
189 |
|
|
if (bio == NULL) { |
190 |
|
|
warnx("BIO_new"); |
191 |
|
|
goto out; |
192 |
|
|
} else if (!X509V3_EXT_print(bio, ex, 0, 0)) { |
193 |
|
|
warnx("X509V3_EXT_print"); |
194 |
|
|
goto out; |
195 |
|
|
} else if ((san = calloc(1, bio->num_write + 1)) == NULL) { |
196 |
|
|
warn("calloc"); |
197 |
|
|
goto out; |
198 |
|
|
} |
199 |
|
|
ssz = BIO_read(bio, san, bio->num_write); |
200 |
|
|
if (ssz < 0 || (unsigned)ssz != bio->num_write) { |
201 |
|
|
warnx("BIO_read"); |
202 |
|
|
goto out; |
203 |
|
|
} |
204 |
|
|
} |
205 |
|
|
|
206 |
|
|
if (san == NULL) { |
207 |
|
|
warnx("%s/%s: does not have a SAN entry", certdir, certfile); |
208 |
|
|
goto out; |
209 |
|
|
} |
210 |
|
|
|
211 |
|
|
/* An array of buckets: the number of entries found. */ |
212 |
|
|
|
213 |
|
|
if ((found = calloc(altsz, sizeof(size_t))) == NULL) { |
214 |
|
|
warn("calloc"); |
215 |
|
|
goto out; |
216 |
|
|
} |
217 |
|
|
|
218 |
|
|
/* |
219 |
|
|
* Parse the SAN line. |
220 |
|
|
* Make sure that all of the domains are represented only once. |
221 |
|
|
*/ |
222 |
|
|
|
223 |
|
|
str = san; |
224 |
|
|
while ((tok = strsep(&str, ",")) != NULL) { |
225 |
|
|
if (*tok == '\0') |
226 |
|
|
continue; |
227 |
|
|
while (isspace((int)*tok)) |
228 |
|
|
tok++; |
229 |
|
|
if (strncmp(tok, "DNS:", 4)) |
230 |
|
|
continue; |
231 |
|
|
tok += 4; |
232 |
|
|
for (j = 0; j < altsz; j++) |
233 |
|
|
if (strcmp(tok, alts[j]) == 0) |
234 |
|
|
break; |
235 |
|
|
if (j == altsz) { |
236 |
|
|
warnx("%s/%s: unknown SAN entry: %s", |
237 |
|
|
certdir, certfile, tok); |
238 |
|
|
goto out; |
239 |
|
|
} |
240 |
|
|
if (found[j]++) { |
241 |
|
|
warnx("%s/%s: duplicate SAN entry: %s", |
242 |
|
|
certdir, certfile, tok); |
243 |
|
|
goto out; |
244 |
|
|
} |
245 |
|
|
} |
246 |
|
|
|
247 |
|
|
for (j = 0; j < altsz; j++) { |
248 |
|
|
if (found[j]) |
249 |
|
|
continue; |
250 |
|
|
warnx("%s/%s: domain not listed: %s", |
251 |
|
|
certdir, certfile, alts[j]); |
252 |
|
|
goto out; |
253 |
|
|
} |
254 |
|
|
|
255 |
|
|
/* |
256 |
|
|
* If we're going to revoke, write the certificate to the |
257 |
|
|
* netproc in DER and base64-encoded format. |
258 |
|
|
* Then exit: we have nothing left to do. |
259 |
|
|
*/ |
260 |
|
|
|
261 |
|
|
if (revocate) { |
262 |
|
|
dodbg("%s/%s: revocation", certdir, certfile); |
263 |
|
|
|
264 |
|
|
/* |
265 |
|
|
* First, tell netproc we're online. |
266 |
|
|
* If they're down, then just exit without warning. |
267 |
|
|
*/ |
268 |
|
|
|
269 |
|
|
cc = writeop(fd, COMM_REVOKE_RESP, REVOKE_EXP); |
270 |
|
|
if (cc == 0) |
271 |
|
|
rc = 1; |
272 |
|
|
if (cc <= 0) |
273 |
|
|
goto out; |
274 |
|
|
|
275 |
|
|
if ((len = i2d_X509(x, NULL)) < 0) { |
276 |
|
|
warnx("i2d_X509"); |
277 |
|
|
goto out; |
278 |
|
|
} else if ((der = dercp = malloc(len)) == NULL) { |
279 |
|
|
warn("malloc"); |
280 |
|
|
goto out; |
281 |
|
|
} else if (len != i2d_X509(x, (u_char **)&dercp)) { |
282 |
|
|
warnx("i2d_X509"); |
283 |
|
|
goto out; |
284 |
|
|
} else if ((der64 = base64buf_url(der, len)) == NULL) { |
285 |
|
|
warnx("base64buf_url"); |
286 |
|
|
goto out; |
287 |
|
|
} else if (writestr(fd, COMM_CSR, der64) >= 0) |
288 |
|
|
rc = 1; |
289 |
|
|
|
290 |
|
|
goto out; |
291 |
|
|
} |
292 |
|
|
|
293 |
|
|
rop = time(NULL) >= (t - RENEW_ALLOW) ? REVOKE_EXP : REVOKE_OK; |
294 |
|
|
|
295 |
|
|
if (rop == REVOKE_EXP) |
296 |
|
|
dodbg("%s/%s: certificate renewable: %lld days left", |
297 |
|
|
certdir, certfile, |
298 |
|
|
(long long)(t - time(NULL)) / 24 / 60 / 60); |
299 |
|
|
else |
300 |
|
|
dodbg("%s/%s: certificate valid: %lld days left", |
301 |
|
|
certdir, certfile, |
302 |
|
|
(long long)(t - time(NULL)) / 24 / 60 / 60); |
303 |
|
|
|
304 |
|
|
if (rop == REVOKE_OK && force) { |
305 |
|
|
warnx("%s/%s: forcing renewal", certdir, certfile); |
306 |
|
|
rop = REVOKE_EXP; |
307 |
|
|
} |
308 |
|
|
|
309 |
|
|
/* |
310 |
|
|
* We can re-submit it given RENEW_ALLOW time before. |
311 |
|
|
* If netproc is down, just exit. |
312 |
|
|
*/ |
313 |
|
|
|
314 |
|
|
if ((cc = writeop(fd, COMM_REVOKE_RESP, rop)) == 0) |
315 |
|
|
rc = 1; |
316 |
|
|
if (cc <= 0) |
317 |
|
|
goto out; |
318 |
|
|
|
319 |
|
|
op = REVOKE__MAX; |
320 |
|
|
if ((lval = readop(fd, COMM_REVOKE_OP)) == 0) |
321 |
|
|
op = REVOKE_STOP; |
322 |
|
|
else if (lval == REVOKE_CHECK) |
323 |
|
|
op = lval; |
324 |
|
|
|
325 |
|
|
if (op == REVOKE__MAX) { |
326 |
|
|
warnx("unknown operation from netproc"); |
327 |
|
|
goto out; |
328 |
|
|
} else if (op == REVOKE_STOP) { |
329 |
|
|
rc = 1; |
330 |
|
|
goto out; |
331 |
|
|
} |
332 |
|
|
|
333 |
|
|
rc = 1; |
334 |
|
|
out: |
335 |
|
|
close(fd); |
336 |
|
|
if (f != NULL) |
337 |
|
|
fclose(f); |
338 |
|
|
if (x != NULL) |
339 |
|
|
X509_free(x); |
340 |
|
|
if (bio != NULL) |
341 |
|
|
BIO_free(bio); |
342 |
|
|
free(san); |
343 |
|
|
free(path); |
344 |
|
|
free(der); |
345 |
|
|
free(found); |
346 |
|
|
free(der64); |
347 |
|
|
ERR_print_errors_fp(stderr); |
348 |
|
|
ERR_free_strings(); |
349 |
|
|
return rc; |
350 |
|
|
} |