summaryrefslogtreecommitdiff
path: root/src/usr.sbin/ocspcheck/ocspcheck.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/usr.sbin/ocspcheck/ocspcheck.c635
1 files changed, 635 insertions, 0 deletions
diff --git a/src/usr.sbin/ocspcheck/ocspcheck.c b/src/usr.sbin/ocspcheck/ocspcheck.c
new file mode 100644
index 0000000000..77fc4e5939
--- /dev/null
+++ b/src/usr.sbin/ocspcheck/ocspcheck.c
@@ -0,0 +1,635 @@
1/*
2 * Copyright (c) 2017 Bob Beck <beck@openbsd.org>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17#include <arpa/inet.h>
18#include <sys/socket.h>
19
20#include <err.h>
21#include <fcntl.h>
22#include <limits.h>
23#include <netdb.h>
24#include <poll.h>
25#include <stdio.h>
26#include <stdlib.h>
27#include <string.h>
28#include <time.h>
29#include <unistd.h>
30
31#include <openssl/err.h>
32#include <openssl/ocsp.h>
33#include <openssl/ssl.h>
34
35#include "http.h"
36
37#define MAXAGE_SEC (14*24*60*60)
38#define JITTER_SEC (60)
39
40typedef struct ocsp_request {
41 STACK_OF(X509) *fullchain;
42 OCSP_REQUEST * req;
43 char *url;
44 unsigned char *data;
45 size_t size;
46 int nonce;
47} ocsp_request;
48
49int verbose;
50#define vspew(fmt, ...) \
51 do { if (verbose >= 1) fprintf(stderr, fmt, __VA_ARGS__); } while (0)
52#define dspew(fmt, ...) \
53 do { if (verbose >= 2) fprintf(stderr, fmt, __VA_ARGS__); } while (0)
54
55#define MAX_SERVERS_DNS 8
56
57struct addr {
58 int family; /* 4 for PF_INET, 6 for PF_INET6 */
59 char ip[INET6_ADDRSTRLEN];
60};
61
62static ssize_t
63host_dns(const char *s, struct addr vec[MAX_SERVERS_DNS])
64{
65 struct addrinfo hints, *res0, *res;
66 int error;
67 ssize_t vecsz;
68 struct sockaddr *sa;
69
70 memset(&hints, 0, sizeof(hints));
71 hints.ai_family = PF_UNSPEC;
72 hints.ai_socktype = SOCK_DGRAM; /* DUMMY */
73 /* ntpd MUST NOT use AI_ADDRCONFIG here */
74
75 error = getaddrinfo(s, NULL, &hints, &res0);
76
77 if (error == EAI_AGAIN ||
78 error == EAI_NODATA ||
79 error == EAI_NONAME)
80 return(0);
81
82 if (error) {
83 warnx("%s: parse error: %s",
84 s, gai_strerror(error));
85 return(-1);
86 }
87
88 for (vecsz = 0, res = res0;
89 NULL != res && vecsz < MAX_SERVERS_DNS;
90 res = res->ai_next) {
91 if (res->ai_family != AF_INET &&
92 res->ai_family != AF_INET6)
93 continue;
94
95 sa = res->ai_addr;
96
97 if (AF_INET == res->ai_family) {
98 vec[vecsz].family = 4;
99 inet_ntop(AF_INET,
100 &(((struct sockaddr_in *)sa)->sin_addr),
101 vec[vecsz].ip, INET6_ADDRSTRLEN);
102 } else {
103 vec[vecsz].family = 6;
104 inet_ntop(AF_INET6,
105 &(((struct sockaddr_in6 *)sa)->sin6_addr),
106 vec[vecsz].ip, INET6_ADDRSTRLEN);
107 }
108
109 dspew("DNS returns %s for %s\n", vec[vecsz].ip, s);
110 vecsz++;
111 break;
112 }
113
114 freeaddrinfo(res0);
115 return(vecsz);
116}
117
118/*
119 * Extract the domain and port from a URL.
120 * The url must be formatted as schema://address[/stuff].
121 * This returns NULL on failure.
122 */
123static char *
124url2host(const char *host, short *port, char **path)
125{
126 char *url, *ep;
127
128 /* We only understand HTTP and HTTPS. */
129
130 if (0 == strncmp(host, "https://", 8)) {
131 *port = 443;
132 if (NULL == (url = strdup(host + 8))) {
133 warn("strdup");
134 return (NULL);
135 }
136 } else if (0 == strncmp(host, "http://", 7)) {
137 *port = 80;
138 if (NULL == (url = strdup(host + 7))) {
139 warn("strdup");
140 return (NULL);
141 }
142 } else {
143 warnx("%s: unknown schema", host);
144 return (NULL);
145 }
146
147 /* Terminate path part. */
148
149 if (NULL != (ep = strchr(url, '/'))) {
150 *path = strdup(ep);
151 *ep = '\0';
152 } else
153 *path = strdup("");
154
155 if (NULL == *path) {
156 warn("strdup");
157 free(url);
158 return (NULL);
159 }
160
161 return (url);
162}
163
164static time_t
165parse_ocsp_time(ASN1_GENERALIZEDTIME *gt)
166{
167 struct tm tm;
168 time_t rv = -1;
169
170 if (gt == NULL)
171 return -1;
172 /* RFC 6960 specifies that all times in OCSP must be GENERALIZEDTIME */
173 if (ASN1_time_parse(gt->data, gt->length, &tm,
174 V_ASN1_GENERALIZEDTIME) == -1)
175 return -1;
176 if ((rv = timegm(&tm)) == -1)
177 return -1;
178 return rv;
179}
180
181static X509_STORE *
182read_cacerts(char *file)
183{
184 X509_STORE *store;
185 X509_LOOKUP *lookup;
186
187 if ((store = X509_STORE_new()) == NULL) {
188 warnx("Malloc failed");
189 goto end;
190 }
191 if ((lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file())) ==
192 NULL) {
193 warnx("Unable to load CA certs from file %s\n", file);
194 goto end;
195 }
196 if (file) {
197 if (!X509_LOOKUP_load_file(lookup, file, X509_FILETYPE_PEM)) {
198 warnx("Unable to load CA certs from file %s\n", file);
199 goto end;
200 }
201 } else
202 X509_LOOKUP_load_file(lookup, NULL, X509_FILETYPE_DEFAULT);
203
204 if ((lookup = X509_STORE_add_lookup(store, X509_LOOKUP_hash_dir())) ==
205 NULL) {
206 warnx("Unable to load CA certs from file %s\n", file);
207 goto end;
208 }
209 X509_LOOKUP_add_dir(lookup, NULL, X509_FILETYPE_DEFAULT);
210 ERR_clear_error();
211 return store;
212
213end:
214 X509_STORE_free(store);
215 return NULL;
216}
217
218static STACK_OF(X509) *
219read_fullchain(const char *file, int *count)
220{
221 int i;
222 BIO *bio;
223 STACK_OF(X509_INFO) *xis = NULL;
224 X509_INFO *xi;
225 STACK_OF(X509) *rv = NULL;
226
227 *count = 0;
228
229 if ((bio = BIO_new_file(file, "r")) == NULL) {
230 warnx("Error opening %s\n", file);
231 ERR_print_errors_fp(stderr);
232 return NULL;
233 }
234 if ((xis = PEM_X509_INFO_read_bio(bio, NULL, NULL, NULL)) == NULL) {
235 warnx("Unable to read PEM format from %s\n", file);
236 ERR_print_errors_fp(stderr);
237 return NULL;
238 }
239 BIO_free(bio);
240
241 if (sk_X509_INFO_num(xis) <= 0) {
242 warnx("No certificates in file %s\n", file);
243 goto end;
244 }
245 if ((rv = sk_X509_new_null()) == NULL) {
246 ERR_print_errors_fp(stderr);
247 goto end;
248 }
249
250 for (i = 0; i < sk_X509_INFO_num(xis); i++) {
251 xi = sk_X509_INFO_value(xis, i);
252 if (xi->x509 == NULL)
253 continue;
254 if (!sk_X509_push(rv, xi->x509)) {
255 ERR_print_errors_fp(stderr);
256 sk_X509_pop_free(rv, X509_free);
257 rv = NULL;
258 goto end;
259 }
260 xi->x509 = NULL;
261 (*count)++;
262 }
263end:
264 sk_X509_INFO_pop_free(xis, X509_INFO_free);
265 return rv;
266}
267
268static inline X509 *
269cert_from_chain(STACK_OF(X509) *fullchain)
270{
271 return sk_X509_value(fullchain, 0);
272}
273
274static X509 *
275issuer_from_chain(STACK_OF(X509) *fullchain)
276{
277 X509 *cert, *issuer;
278 X509_NAME *issuer_name;
279
280 cert = cert_from_chain(fullchain);
281 if ((issuer_name = X509_get_issuer_name(cert)) == NULL)
282 return NULL;
283
284 issuer = X509_find_by_subject(fullchain, issuer_name);
285 return issuer;
286}
287
288static ocsp_request *
289ocsp_request_new_from_cert(char *file, int nonce)
290{
291 X509 *cert = NULL;
292 int count = 0;
293 OCSP_CERTID *id;
294 ocsp_request *request;
295 const EVP_MD *cert_id_md = NULL;
296 X509 *issuer = NULL;
297 STACK_OF(OPENSSL_STRING) *urls;
298
299 if ((request = calloc(1, sizeof(ocsp_request))) == NULL) {
300 warn("malloc");
301 return NULL;
302 }
303
304 if ((request->req = OCSP_REQUEST_new()) == NULL)
305 return NULL;
306
307 request->fullchain = read_fullchain(file, &count);
308 /* Drop rpath from pledge, we don't need to read anymore */
309 if (pledge("stdio inet dns", NULL) == -1)
310 err(EXIT_FAILURE, "pledge");
311
312 if (request->fullchain == NULL)
313 return NULL;
314 if (count <= 1) {
315 warnx("File %s does not contain a cert chain",
316 file);
317 return NULL;
318 }
319 if ((cert = cert_from_chain(request->fullchain)) == NULL) {
320 warnx("No certificate found in %s", file);
321 return NULL;
322 }
323 if ((issuer = issuer_from_chain(request->fullchain)) == NULL) {
324 warnx("Unable to find issuer for cert in %s", file);
325 return NULL;
326 }
327
328 urls = X509_get1_ocsp(cert);
329 if (urls == NULL || sk_OPENSSL_STRING_num(urls) <= 0) {
330 warnx("Certificate in %s contains no OCSP url", file);
331 return NULL;
332 }
333 if ((request->url = strdup(sk_OPENSSL_STRING_value(urls, 0))) == NULL)
334 return NULL;
335 X509_email_free(urls);
336
337 cert_id_md = EVP_sha1(); /* XXX. This sucks but OCSP is poopy */
338 if ((id = OCSP_cert_to_id(cert_id_md, cert, issuer)) == NULL) {
339 warnx("Unable to get certificate id from cert in %s", file);
340 ERR_print_errors_fp(stderr);
341 return NULL;
342 }
343 if (OCSP_request_add0_id(request->req, id) == NULL) {
344 warnx("Unable to add certificate id to request");
345 ERR_print_errors_fp(stderr);
346 return NULL;
347 }
348
349 request->nonce = nonce;
350 if (request->nonce)
351 OCSP_request_add1_nonce(request->req, NULL, -1);
352
353 if ((request->size = i2d_OCSP_REQUEST(request->req,
354 &request->data)) <= 0) {
355 warnx("Unable to encode ocsp request");
356 return NULL;
357 }
358 if (request->data == NULL) {
359 warnx("Unable to allocte memory");
360 return NULL;
361 }
362 return(request);
363}
364
365
366int
367validate_response(char *buf, size_t size, ocsp_request *request,
368 X509_STORE *store, char *host, char *file)
369{
370 ASN1_GENERALIZEDTIME *revtime = NULL, *thisupd = NULL, *nextupd = NULL;
371 const unsigned char **p = (const unsigned char **)&buf;
372 int status, cert_status=0, crl_reason=0;
373 time_t now, rev_t = -1, this_t, next_t;
374 OCSP_RESPONSE *resp;
375 OCSP_BASICRESP *bresp;
376 OCSP_CERTID *cid;
377 X509 *cert, *issuer;
378
379 if ((cert = cert_from_chain(request->fullchain)) == NULL) {
380 warnx("No certificate found in %s", file);
381 return 0;
382 }
383 if ((issuer = issuer_from_chain(request->fullchain)) == NULL) {
384 warnx("Unable to find certificate issuer for cert in %s",
385 file);
386 return 0;
387 }
388 if ((cid = OCSP_cert_to_id(NULL, cert, issuer)) == NULL) {
389 warnx("Unable to get issuer cert/CID in %s", file);
390 return(0);
391 }
392
393 if ((resp = d2i_OCSP_RESPONSE(NULL, p, size)) == NULL) {
394 warnx("OCSP response unserializable from host %s", host);
395 return 0;
396 }
397
398 if ((bresp = OCSP_response_get1_basic(resp)) == NULL) {
399 warnx("Failed to load OCSP response from %s", host);
400 return(0);
401 }
402
403 if (OCSP_basic_verify(bresp, request->fullchain, store,
404 OCSP_TRUSTOTHER) != 1) {
405 ERR_print_errors_fp(stderr);
406 warnx("OCSP verify failed from %s", host);
407 return 0;
408 }
409 dspew("OCSP response signature validated from %s\n", host);
410
411 status = OCSP_response_status(resp);
412 if (status != OCSP_RESPONSE_STATUS_SUCCESSFUL) {
413 warnx("OCSP Failure: code %d (%s) from host %s",
414 status, OCSP_response_status_str(status), host);
415 return(0);
416 }
417 dspew("OCSP response status %d from host %s\n", status, host);
418
419 /* Check the nonce if we sent one */
420
421 if (request->nonce) {
422 if (OCSP_check_nonce(request->req, bresp) <= 0) {
423 warnx("No OCSP nonce, or mismatch, from host %s", host);
424 return 0;
425 }
426 }
427
428 if (OCSP_resp_find_status(bresp, cid, &cert_status, &crl_reason,
429 &revtime, &thisupd, &nextupd) != 1) {
430 warnx("OCSP verify failed: no result for cert");
431 return 0;
432 }
433
434 if (revtime && (rev_t = parse_ocsp_time(revtime)) == -1) {
435 warnx("Unable to parse revocation time in OCSP reply");
436 return 0;
437 }
438 /*
439 * Belt and suspenders, Treat it as revoked if there is either
440 * a revocation time, or status revoked.
441 */
442 if (rev_t != -1 || cert_status == V_OCSP_CERTSTATUS_REVOKED) {
443 warnx("Invalid OCSP reply: certificate is revoked");
444 if (rev_t != -1)
445 warnx("Certificate revoked at: %s", ctime(&rev_t));
446 return 0;
447 }
448 if ((this_t = parse_ocsp_time(thisupd)) == -1) {
449 warnx("unable to parse this update time in OCSP reply");
450 return 0;
451 }
452 if ((next_t = parse_ocsp_time(nextupd)) == -1) {
453 warnx("unable to parse next update time in OCSP reply");
454 return 0;
455 }
456
457 /* Don't allow this update to precede next update */
458 if (this_t >= next_t) {
459 warnx("Invalid OCSP reply: this update >= next update");
460 return 0;
461 }
462
463 now = time(NULL);
464 /*
465 * Check that this update is not more than JITTER seconds
466 * in the future.
467 */
468 if (this_t > now + JITTER_SEC) {
469 warnx("Invalid OCSP reply: this update is in the future (%s)",
470 ctime(&this_t));
471 return 0;
472 }
473
474 /*
475 * Check that this update is not more than MAXSEC
476 * in the past.
477 */
478 if (this_t < now - MAXAGE_SEC) {
479 warnx("Invalid OCSP reply: this update is too old (%s)",
480 ctime(&this_t));
481 return 0;
482 }
483
484 /*
485 * Check that next update is still valid
486 */
487 if (next_t < now - JITTER_SEC) {
488 warnx("Invalid OCSP reply: reply has expired (%s)",
489 ctime(&next_t));
490 return 0;
491 }
492
493 vspew("OCSP response validated from %s\n", host);
494 vspew(" This Update: %s", ctime(&this_t));
495 vspew(" Next Update: %s", ctime(&next_t));
496 return 1;
497}
498
499static void
500usage(void)
501{
502 errx(1, "Usage: %s [-N] [-v] [-o staplefile] certfile", getprogname());
503}
504
505int
506main (int argc, char **argv)
507{
508 char *host = NULL, *path = "/", *certfile = NULL, *outfile = NULL,
509 *cafile = NULL;
510 struct addr addrs[MAX_SERVERS_DNS] = { };
511 struct source sources[MAX_SERVERS_DNS];
512 int i, ch, staplefd = -1, nonce = 1;
513 ocsp_request *request = NULL;
514 size_t rescount, httphsz;
515 struct httphead *httph;
516 struct httpget *hget;
517 X509_STORE *castore;
518 ssize_t written, w;
519 short port;
520
521 while ((ch = getopt(argc, argv, "C:No:v")) != -1) {
522 switch (ch) {
523 case 'C':
524 cafile = optarg;
525 break;
526 case 'N':
527 nonce = 0;
528 break;
529 case 'o':
530 outfile = optarg;
531 break;
532 case 'v':
533 verbose++;
534 break;
535 default:
536 usage();
537 }
538 }
539 argc -= optind;
540 argv += optind;
541
542 if ((certfile = argv[0]) == NULL)
543 usage();
544
545 if (outfile != NULL) {
546 if (strcmp(outfile, "-") == 0)
547 staplefd = STDOUT_FILENO;
548 else
549 staplefd = open(outfile, O_WRONLY|O_CREAT);
550 if (staplefd < 0)
551 err(EXIT_FAILURE, "Unable to open output file %s",
552 outfile);
553 }
554
555 if (pledge("stdio inet rpath dns", NULL) == -1)
556 err(EXIT_FAILURE, "pledge");
557
558 /*
559 * Load our certificate and keystore, and build up an
560 * OSCP request based on the full certificate chain
561 * we have been given to check.
562 */
563 if ((castore = read_cacerts(NULL)) == NULL)
564 errx(EXIT_FAILURE, "Unable to load %s", cafile);
565
566 if ((request = ocsp_request_new_from_cert(certfile, nonce)) == NULL)
567 errx(EXIT_FAILURE, "Unable to build OCSP request");
568
569 if ((host = url2host(request->url, &port, &path)) == NULL)
570 errx(EXIT_FAILURE, "Invalid OCSP url %s from %s", request->url,
571 certfile);
572 dspew("Built an %ld byte ocsp request\n", request->size);
573 vspew("Using %s to host %s, port %d, path %s\n",
574 port == 443 ? "https" : "http", host, port, path);
575
576 rescount = host_dns(host, addrs);
577 for (i = 0; i < rescount; i++) {
578 sources[i].ip = addrs[i].ip;
579 sources[i].family = addrs[i].family;
580 }
581
582 /*
583 * Do an HTTP post to send our request to the OCSP
584 * server, and hopefully get an answer back
585 */
586 hget = http_get(sources, rescount, host, port, path,
587 request->data, request->size);
588 if (hget == NULL)
589 errx(EXIT_FAILURE, "http_get");
590 httph = http_head_parse(hget->http, hget->xfer, &httphsz);
591 dspew("Server at %s returns:\n", host);
592 for (i = 0; i < httphsz; i++)
593 dspew(" [%s]=[%s]\n", httph[i].key, httph[i].val);
594 dspew(" [Body]=[%ld bytes]\n", hget->bodypartsz);
595 if (hget->bodypartsz <= 0)
596 errx(EXIT_FAILURE, "No body in reply from %s", host);
597
598 /*
599 * Pledge minimally before fiddling with libcrypto init routines
600 * and untrusted input from someone's OCSP server.
601 */
602
603 if (pledge("stdio", NULL) == -1)
604 err(EXIT_FAILURE, "pledge");
605
606 /*
607 * Validate the OCSP response we got back
608 */
609 ERR_load_crypto_strings();
610 OPENSSL_add_all_algorithms_noconf();
611 if (!validate_response(hget->bodypart, hget->bodypartsz,
612 request, castore, host, certfile))
613 errx(EXIT_FAILURE, "Can not validate %s", certfile);
614
615 /*
616 * If we have been given a place to save a staple,
617 * write out the DER format response to the staplefd
618 */
619 if (staplefd >= 0) {
620 ftruncate(staplefd, 0);
621 w = 0 ;
622 written = 0;
623 while (written < hget->bodypartsz) {
624 w = write(staplefd, hget->bodypart + written,
625 hget->bodypartsz - written);
626 if (w == -1) {
627 if (errno != EINTR && errno != EAGAIN)
628 err(1, "Write of OCSP response failed");
629 } else
630 written += w;
631 }
632 close(staplefd);
633 }
634 exit(EXIT_SUCCESS);
635}