GCC Code Coverage Report
Directory: ./ Exec Total Coverage
File: usr.sbin/ocspcheck/ocspcheck.c Lines: 0 301 0.0 %
Date: 2017-11-07 Branches: 0 197 0.0 %

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