From f748422c1c8c084effc6cacbef92cb4ee75c13d6 Mon Sep 17 00:00:00 2001 From: beck <> Date: Tue, 24 Jan 2017 08:50:57 +0000 Subject: New ocspcheck utility to validate a certificate against its ocsp responder and save the reply for stapling ok deraadt@ jsing@ --- src/usr.sbin/ocspcheck/Makefile | 24 ++ src/usr.sbin/ocspcheck/http.c | 782 +++++++++++++++++++++++++++++++++++++ src/usr.sbin/ocspcheck/http.h | 96 +++++ src/usr.sbin/ocspcheck/ocspcheck.8 | 97 +++++ src/usr.sbin/ocspcheck/ocspcheck.c | 635 ++++++++++++++++++++++++++++++ 5 files changed, 1634 insertions(+) create mode 100644 src/usr.sbin/ocspcheck/Makefile create mode 100644 src/usr.sbin/ocspcheck/http.c create mode 100644 src/usr.sbin/ocspcheck/http.h create mode 100644 src/usr.sbin/ocspcheck/ocspcheck.8 create mode 100644 src/usr.sbin/ocspcheck/ocspcheck.c (limited to 'src') diff --git a/src/usr.sbin/ocspcheck/Makefile b/src/usr.sbin/ocspcheck/Makefile new file mode 100644 index 0000000000..55d9b5b763 --- /dev/null +++ b/src/usr.sbin/ocspcheck/Makefile @@ -0,0 +1,24 @@ +# $OpenBSD: Makefile,v 1.1 2017/01/24 08:50:57 beck Exp $ + +PROG= ocspcheck + +LDADD= -ltls -lssl -lcrypto +DPADD= ${LIBTLS} ${LIBSSL} ${LIBCRYPTO} +MAN= ocspcheck.8 + +CFLAGS+= -Wall -Werror +CFLAGS+= -Wformat +CFLAGS+= -Wformat-security +CFLAGS+= -Wimplicit +CFLAGS+= -Wreturn-type +CFLAGS+= -Wshadow +CFLAGS+= -Wtrigraphs +CFLAGS+= -Wuninitialized +CFLAGS+= -Wunused + +CFLAGS+= -DLIBRESSL_INTERNAL + +SRCS= ocspcheck.c http.c + + +.include diff --git a/src/usr.sbin/ocspcheck/http.c b/src/usr.sbin/ocspcheck/http.c new file mode 100644 index 0000000000..3c0f404c31 --- /dev/null +++ b/src/usr.sbin/ocspcheck/http.c @@ -0,0 +1,782 @@ +/* $Id: http.c,v 1.1 2017/01/24 08:50:57 beck Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "http.h" +#include + +#define DEFAULT_CA_FILE "/etc/ssl/cert.pem" + +/* + * A buffer for transferring HTTP/S data. + */ +struct httpxfer { + char *hbuf; /* header transfer buffer */ + size_t hbufsz; /* header buffer size */ + int headok; /* header has been parsed */ + char *bbuf; /* body transfer buffer */ + size_t bbufsz; /* body buffer size */ + int bodyok; /* body has been parsed */ + char *headbuf; /* lookaside buffer for headers */ + struct httphead *head; /* parsed headers */ + size_t headsz; /* number of headers */ +}; + +/* + * An HTTP/S connection object. + */ +struct http { + int fd; /* connected socket */ + short port; /* port number */ + struct source src; /* endpoint (raw) host */ + char *path; /* path to request */ + char *host; /* name of endpoint host */ + struct tls *ctx; /* if TLS */ + writefp writer; /* write function */ + readfp reader; /* read function */ +}; + +struct tls_config *tlscfg; + +static ssize_t +dosysread(char *buf, size_t sz, const struct http *http) +{ + ssize_t rc; + + rc = read(http->fd, buf, sz); + if (rc < 0) + warn("%s: read", http->src.ip); + return (rc); +} + +static ssize_t +dosyswrite(const void *buf, size_t sz, const struct http *http) +{ + ssize_t rc; + + rc = write(http->fd, buf, sz); + if (rc < 0) + warn("%s: write", http->src.ip); + return (rc); +} + +static ssize_t +dotlsread(char *buf, size_t sz, const struct http *http) +{ + ssize_t rc; + + do { + rc = tls_read(http->ctx, buf, sz); + } while (TLS_WANT_POLLIN == rc || TLS_WANT_POLLOUT == rc); + + if (rc < 0) + warnx("%s: tls_read: %s", http->src.ip, + tls_error(http->ctx)); + return (rc); +} + +static ssize_t +dotlswrite(const void *buf, size_t sz, const struct http *http) +{ + ssize_t rc; + + do { + rc = tls_write(http->ctx, buf, sz); + } while (TLS_WANT_POLLIN == rc || TLS_WANT_POLLOUT == rc); + + if (rc < 0) + warnx("%s: tls_write: %s", http->src.ip, + tls_error(http->ctx)); + return (rc); +} + +int +http_init() +{ + if (NULL != tlscfg) + return (0); + + if (-1 == tls_init()) { + warn("tls_init"); + goto err; + } + + tlscfg = tls_config_new(); + if (NULL == tlscfg) { + warn("tls_config_new"); + goto err; + } + + if (-1 == tls_config_set_ca_file(tlscfg, DEFAULT_CA_FILE)) { + warn("tls_config_set_ca_file: %s", tls_config_error(tlscfg)); + goto err; + } + + return (0); + + err: + tls_config_free(tlscfg); + tlscfg = NULL; + + return (-1); +} + +static ssize_t +http_read(char *buf, size_t sz, const struct http *http) +{ + ssize_t ssz, xfer; + + xfer = 0; + do { + if ((ssz = http->reader(buf, sz, http)) < 0) + return (-1); + if (0 == ssz) + break; + xfer += ssz; + sz -= ssz; + buf += ssz; + } while (ssz > 0 && sz > 0); + + return (xfer); +} + +static int +http_write(const char *buf, size_t sz, const struct http *http) +{ + ssize_t ssz, xfer; + + xfer = sz; + while (sz > 0) { + if ((ssz = http->writer(buf, sz, http)) < 0) + return (-1); + sz -= ssz; + buf += (size_t)ssz; + } + return (xfer); +} + +void +http_disconnect(struct http *http) +{ + int rc; + + if (NULL != http->ctx) { + /* TLS connection. */ + do { + rc = tls_close(http->ctx); + } while (TLS_WANT_POLLIN == rc || TLS_WANT_POLLOUT == rc); + + if (rc < 0) + warnx("%s: tls_close: %s", http->src.ip, + tls_error(http->ctx)); + + tls_free(http->ctx); + } + if (-1 != http->fd) { + if (-1 == close(http->fd)) + warn("%s: close", http->src.ip); + } + + http->fd = -1; + http->ctx = NULL; +} + +void +http_free(struct http *http) +{ + + if (NULL == http) + return; + http_disconnect(http); + free(http->host); + free(http->path); + free(http->src.ip); + free(http); +} + +struct http * +http_alloc(const struct source *addrs, size_t addrsz, + const char *host, short port, const char *path) +{ + struct sockaddr_storage ss; + int family, fd, c; + socklen_t len; + size_t cur, i = 0; + struct http *http; + + /* Do this while we still have addresses to connect. */ +again: + if (i == addrsz) + return (NULL); + cur = i++; + + /* Convert to PF_INET or PF_INET6 address from string. */ + + memset(&ss, 0, sizeof(struct sockaddr_storage)); + + if (4 == addrs[cur].family) { + family = PF_INET; + ((struct sockaddr_in *)&ss)->sin_family = AF_INET; + ((struct sockaddr_in *)&ss)->sin_port = htons(port); + c = inet_pton(AF_INET, addrs[cur].ip, + &((struct sockaddr_in *)&ss)->sin_addr); + len = sizeof(struct sockaddr_in); + } else if (6 == addrs[cur].family) { + family = PF_INET6; + ((struct sockaddr_in6 *)&ss)->sin6_family = AF_INET6; + ((struct sockaddr_in6 *)&ss)->sin6_port = htons(port); + c = inet_pton(AF_INET6, addrs[cur].ip, + &((struct sockaddr_in6 *)&ss)->sin6_addr); + len = sizeof(struct sockaddr_in6); + } else { + warnx("%s: unknown family", addrs[cur].ip); + goto again; + } + + if (c < 0) { + warn("%s: inet_ntop", addrs[cur].ip); + goto again; + } else if (0 == c) { + warnx("%s: inet_ntop", addrs[cur].ip); + goto again; + } + + /* Create socket and connect. */ + + fd = socket(family, SOCK_STREAM, 0); + if (-1 == fd) { + warn("%s: socket", addrs[cur].ip); + goto again; + } else if (-1 == connect(fd, (struct sockaddr *)&ss, len)) { + warn("%s: connect", addrs[cur].ip); + close(fd); + goto again; + } + + /* Allocate the communicator. */ + + http = calloc(1, sizeof(struct http)); + if (NULL == http) { + warn("calloc"); + close(fd); + return (NULL); + } + http->fd = fd; + http->port = port; + http->src.family = addrs[cur].family; + http->src.ip = strdup(addrs[cur].ip); + http->host = strdup(host); + http->path = strdup(path); + if (NULL == http->src.ip || NULL == http->host || NULL == http->path) { + warn("strdup"); + goto err; + } + + /* If necessary, do our TLS setup. */ + + if (443 != port) { + http->writer = dosyswrite; + http->reader = dosysread; + return (http); + } + + http->writer = dotlswrite; + http->reader = dotlsread; + + if (NULL == (http->ctx = tls_client())) { + warn("tls_client"); + goto err; + } else if (-1 == tls_configure(http->ctx, tlscfg)) { + warnx("%s: tls_configure: %s", + http->src.ip, tls_error(http->ctx)); + goto err; + } + + if (0 != tls_connect_socket(http->ctx, http->fd, http->host)) { + warnx("%s: tls_connect_socket: %s, %s", http->src.ip, + http->host, tls_error(http->ctx)); + goto err; + } + + return (http); +err: + http_free(http); + return (NULL); +} + +struct httpxfer * +http_open(const struct http *http, const void *p, size_t psz) +{ + char *req; + int c; + struct httpxfer *trans; + + if (NULL == p) { + c = asprintf(&req, + "GET %s HTTP/1.0\r\n" + "Host: %s\r\n" + "\r\n", + http->path, http->host); + } else { + c = asprintf(&req, + "POST %s HTTP/1.0\r\n" + "Host: %s\r\n" + "Content-Type: application/ocsp-request\r\n" + "Accept: application/ocsp-response\r\n" + "Content-Length: %zu\r\n" + "\r\n", + http->path, http->host, psz); + } + if (-1 == c) { + warn("asprintf"); + return (NULL); + } else if (!http_write(req, c, http)) { + free(req); + return (NULL); + } else if (NULL != p && ! http_write(p, psz, http)) { + free(req); + return (NULL); + } + + free(req); + + trans = calloc(1, sizeof(struct httpxfer)); + if (NULL == trans) + warn("calloc"); + return (trans); +} + +void +http_close(struct httpxfer *x) +{ + + if (NULL == x) + return; + free(x->hbuf); + free(x->bbuf); + free(x->headbuf); + free(x->head); + free(x); +} + +/* + * Read the HTTP body from the wire. + * If invoked multiple times, this will return the same pointer with the + * same data (or NULL, if the original invocation returned NULL). + * Returns NULL if read or allocation errors occur. + * You must not free the returned pointer. + */ +char * +http_body_read(const struct http *http, struct httpxfer *trans, size_t *sz) +{ + char buf[BUFSIZ]; + ssize_t ssz; + void *pp; + size_t szp; + + if (NULL == sz) + sz = &szp; + + /* Have we already parsed this? */ + + if (trans->bodyok > 0) { + *sz = trans->bbufsz; + return (trans->bbuf); + } else if (trans->bodyok < 0) + return (NULL); + + *sz = 0; + trans->bodyok = -1; + + do { + /* If less than sizeof(buf), at EOF. */ + if ((ssz = http_read(buf, sizeof(buf), http)) < 0) + return (NULL); + else if (0 == ssz) + break; + pp = realloc(trans->bbuf, trans->bbufsz + ssz); + if (NULL == pp) { + warn("realloc"); + return (NULL); + } + trans->bbuf = pp; + memcpy(trans->bbuf + trans->bbufsz, buf, ssz); + trans->bbufsz += ssz; + } while (sizeof(buf) == ssz); + + trans->bodyok = 1; + *sz = trans->bbufsz; + return (trans->bbuf); +} + +struct httphead * +http_head_get(const char *v, struct httphead *h, size_t hsz) +{ + size_t i; + + for (i = 0; i < hsz; i++) { + if (strcmp(h[i].key, v)) + continue; + return (&h[i]); + } + return (NULL); +} + +/* + * Look through the headers and determine our HTTP code. + * This will return -1 on failure, otherwise the code. + */ +int +http_head_status(const struct http *http, struct httphead *h, size_t sz) +{ + int rc; + unsigned int code; + struct httphead *st; + + if (NULL == (st = http_head_get("Status", h, sz))) { + warnx("%s: no status header", http->src.ip); + return (-1); + } + + rc = sscanf(st->val, "%*s %u %*s", &code); + if (rc < 0) { + warn("sscanf"); + return (-1); + } else if (1 != rc) { + warnx("%s: cannot convert status header", http->src.ip); + return (-1); + } + return (code); +} + +/* + * Parse headers from the transfer. + * Malformed headers are skipped. + * A special "Status" header is added for the HTTP status line. + * This can only happen once http_head_read has been called with + * success. + * This can be invoked multiple times: it will only parse the headers + * once and after that it will just return the cache. + * You must not free the returned pointer. + * If the original header parse failed, or if memory allocation fails + * internally, this returns NULL. + */ +struct httphead * +http_head_parse(const struct http *http, struct httpxfer *trans, size_t *sz) +{ + size_t hsz, szp; + struct httphead *h; + char *cp, *ep, *ccp, *buf; + + if (NULL == sz) + sz = &szp; + + /* + * If we've already parsed the headers, return the + * previously-parsed buffer now. + * If we have errors on the stream, return NULL now. + */ + + if (NULL != trans->head) { + *sz = trans->headsz; + return (trans->head); + } else if (trans->headok <= 0) + return (NULL); + + if (NULL == (buf = strdup(trans->hbuf))) { + warn("strdup"); + return (NULL); + } + hsz = 0; + cp = buf; + + do { + if (NULL != (cp = strstr(cp, "\r\n"))) + cp += 2; + hsz++; + } while (NULL != cp); + + /* + * Allocate headers, then step through the data buffer, parsing + * out headers as we have them. + * We know at this point that the buffer is nil-terminated in + * the usual way. + */ + + h = calloc(hsz, sizeof(struct httphead)); + if (NULL == h) { + warn("calloc"); + free(buf); + return (NULL); + } + + *sz = hsz; + hsz = 0; + cp = buf; + + do { + if (NULL != (ep = strstr(cp, "\r\n"))) { + *ep = '\0'; + ep += 2; + } + if (0 == hsz) { + h[hsz].key = "Status"; + h[hsz++].val = cp; + continue; + } + + /* Skip bad headers. */ + if (NULL == (ccp = strchr(cp, ':'))) { + warnx("%s: header without separator", http->src.ip); + continue; + } + + *ccp++ = '\0'; + while (isspace((int)*ccp)) + ccp++; + h[hsz].key = cp; + h[hsz++].val = ccp; + } while (NULL != (cp = ep)); + + trans->headbuf = buf; + trans->head = h; + trans->headsz = hsz; + return (h); +} + +/* + * Read the HTTP headers from the wire. + * If invoked multiple times, this will return the same pointer with the + * same data (or NULL, if the original invocation returned NULL). + * Returns NULL if read or allocation errors occur. + * You must not free the returned pointer. + */ +char * +http_head_read(const struct http *http, struct httpxfer *trans, size_t *sz) +{ + char buf[BUFSIZ]; + ssize_t ssz; + char *ep; + void *pp; + size_t szp; + + if (NULL == sz) + sz = &szp; + + /* Have we already parsed this? */ + + if (trans->headok > 0) { + *sz = trans->hbufsz; + return (trans->hbuf); + } else if (trans->headok < 0) + return (NULL); + + *sz = 0; + ep = NULL; + trans->headok = -1; + + /* + * Begin by reading by BUFSIZ blocks until we reach the header + * termination marker (two CRLFs). + * We might read into our body, but that's ok: we'll copy out + * the body parts into our body buffer afterward. + */ + + do { + /* If less than sizeof(buf), at EOF. */ + if ((ssz = http_read(buf, sizeof(buf), http)) < 0) + return (NULL); + else if (0 == ssz) + break; + pp = realloc(trans->hbuf, trans->hbufsz + ssz); + if (NULL == pp) { + warn("realloc"); + return (NULL); + } + trans->hbuf = pp; + memcpy(trans->hbuf + trans->hbufsz, buf, ssz); + trans->hbufsz += ssz; + /* Search for end of headers marker. */ + ep = memmem(trans->hbuf, trans->hbufsz, "\r\n\r\n", 4); + } while (NULL == ep && sizeof(buf) == ssz); + + if (NULL == ep) { + warnx("%s: partial transfer", http->src.ip); + return (NULL); + } + *ep = '\0'; + + /* + * The header data is invalid if it has any binary characters in + * it: check that now. + * This is important because we want to guarantee that all + * header keys and pairs are properly nil-terminated. + */ + + if (strlen(trans->hbuf) != (uintptr_t)(ep - trans->hbuf)) { + warnx("%s: binary data in header", http->src.ip); + return (NULL); + } + + /* + * Copy remaining buffer into body buffer. + */ + + ep += 4; + trans->bbufsz = (trans->hbuf + trans->hbufsz) - ep; + trans->bbuf = malloc(trans->bbufsz); + if (NULL == trans->bbuf) { + warn("malloc"); + return (NULL); + } + memcpy(trans->bbuf, ep, trans->bbufsz); + + trans->headok = 1; + *sz = trans->hbufsz; + return (trans->hbuf); +} + +void +http_get_free(struct httpget *g) +{ + + if (NULL == g) + return; + http_close(g->xfer); + http_free(g->http); + free(g); +} + +struct httpget * +http_get(const struct source *addrs, size_t addrsz, const char *domain, + short port, const char *path, const void *post, size_t postsz) +{ + struct http *h; + struct httpxfer *x; + struct httpget *g; + struct httphead *head; + size_t headsz, bodsz, headrsz; + int code; + char *bod, *headr; + + h = http_alloc(addrs, addrsz, domain, port, path); + if (NULL == h) + return (NULL); + + if (NULL == (x = http_open(h, post, postsz))) { + http_free(h); + return (NULL); + } else if (NULL == (headr = http_head_read(h, x, &headrsz))) { + http_close(x); + http_free(h); + return (NULL); + } else if (NULL == (bod = http_body_read(h, x, &bodsz))) { + http_close(x); + http_free(h); + return (NULL); + } + + http_disconnect(h); + + if (NULL == (head = http_head_parse(h, x, &headsz))) { + http_close(x); + http_free(h); + return (NULL); + } else if ((code = http_head_status(h, head, headsz)) < 0) { + http_close(x); + http_free(h); + return (NULL); + } + + if (NULL == (g = calloc(1, sizeof(struct httpget)))) { + warn("calloc"); + http_close(x); + http_free(h); + return (NULL); + } + + g->headpart = headr; + g->headpartsz = headrsz; + g->bodypart = bod; + g->bodypartsz = bodsz; + g->head = head; + g->headsz = headsz; + g->code = code; + g->xfer = x; + g->http = h; + return (g); +} + +#if 0 +int +main(void) +{ + struct httpget *g; + struct httphead *httph; + size_t i, httphsz; + struct source addrs[2]; + size_t addrsz; + +#if 0 + addrs[0].ip = "127.0.0.1"; + addrs[0].family = 4; + addrsz = 1; +#else + addrs[0].ip = "2a00:1450:400a:806::2004"; + addrs[0].family = 6; + addrs[1].ip = "193.135.3.123"; + addrs[1].family = 4; + addrsz = 2; +#endif + + if (http_init() == -1) + errx(EXIT_FAILURE, "http_init"); + +#if 0 + g = http_get(addrs, addrsz, "localhost", 80, "/index.html"); +#else + g = http_get(addrs, addrsz, "www.google.ch", 80, "/index.html", + NULL, 0); +#endif + + if (NULL == g) + errx(EXIT_FAILURE, "http_get"); + + httph = http_head_parse(g->http, g->xfer, &httphsz); + warnx("code: %d", g->code); + + for (i = 0; i < httphsz; i++) + warnx("head: [%s]=[%s]", httph[i].key, httph[i].val); + + http_get_free(g); + return (EXIT_SUCCESS); +} +#endif diff --git a/src/usr.sbin/ocspcheck/http.h b/src/usr.sbin/ocspcheck/http.h new file mode 100644 index 0000000000..b4e66f21d3 --- /dev/null +++ b/src/usr.sbin/ocspcheck/http.h @@ -0,0 +1,96 @@ +/* $Id: http.h,v 1.1 2017/01/24 08:50:57 beck Exp $ */ +/* + * Copyright (c) 2016 Kristaps Dzonsons + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifndef HTTP_H +#define HTTP_H + +struct source { + int family; /* 4 (PF_INET) or 6 (PF_INET6) */ + char *ip; /* IPV4 or IPV6 address */ +}; + +struct http; + +/* + * Write and read callbacks to allow HTTP and HTTPS. + * Both of these return the number of bytes read (or written) or -1 on + * failure. + * 0 bytes read means that the connection has closed. + */ +typedef ssize_t (*writefp)(const void *, size_t, const struct http *); +typedef ssize_t (*readfp)(char *, size_t, const struct http *); + +/* + * HTTP/S header pair. + * There's also a cooked-up pair, "Status", with the status code. + * Both strings are nil-terminated. + */ +struct httphead { + const char *key; + const char *val; +}; + +/* + * Grab all information from a transfer. + * DO NOT free any parts of this, and editing the parts (e.g., changing + * the underlying strings) will persist; so in short, don't. + * All of these values will be set upon http_get() success. + */ +struct httpget { + struct httpxfer *xfer; /* underlying transfer */ + struct http *http; /* underlying connection */ + int code; /* return code */ + struct httphead *head; /* headers */ + size_t headsz; /* number of headers */ + char *headpart; /* header buffer */ + size_t headpartsz; /* size of headpart */ + char *bodypart; /* body buffer */ + size_t bodypartsz; /* size of bodypart */ +}; + +__BEGIN_DECLS + +int http_init(void); + +/* Convenience functions. */ +struct httpget *http_get(const struct source *, size_t, + const char *, short, const char *, + const void *, size_t); +void http_get_free(struct httpget *); + +/* Allocation and release. */ +struct http *http_alloc(const struct source *, size_t, + const char *, short, const char *); +void http_free(struct http *); +struct httpxfer *http_open(const struct http *, const void *, size_t); +void http_close(struct httpxfer *); +void http_disconnect(struct http *); + +/* Access. */ +char *http_head_read(const struct http *, + struct httpxfer *, size_t *); +struct httphead *http_head_parse(const struct http *, + struct httpxfer *, size_t *); +char *http_body_read(const struct http *, + struct httpxfer *, size_t *); +int http_head_status(const struct http *, + struct httphead *, size_t); +struct httphead *http_head_get(const char *, + struct httphead *, size_t); + +__END_DECLS + +#endif /* HTTP_H */ diff --git a/src/usr.sbin/ocspcheck/ocspcheck.8 b/src/usr.sbin/ocspcheck/ocspcheck.8 new file mode 100644 index 0000000000..2ef5d26fc3 --- /dev/null +++ b/src/usr.sbin/ocspcheck/ocspcheck.8 @@ -0,0 +1,97 @@ +.\" $OpenBSD: ocspcheck.8,v 1.1 2017/01/24 08:50:57 beck Exp $ +.\" +.\" Copyright (c) 2017 Bob Beck +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate: January 24 2017 $ +.Dt OCSPCHECK 8 +.Os +.Sh NAME +.Nm ocspcheck +.Nd Check a certificate for validity against its OSCP responder +.Sh SYNOPSIS +.Nm +.Op Fl vN +.Op Fl o Ar staplefile +.Op Fl C Ar CAfile +.Ar file +.Sh DESCRIPTION +The +.Nm +utility validates a PEM format certificate against the OCSP responder +encoded in the certificate that is specified by the +.Ar file +argument. +Normally it should be used for checking server certificates +and maintaining saved OCSP responses to be used for OCSP stapling. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl C Ar CAfile +Specify a PEM formatted root certificate bundle to use for the validation of +requests. +By default no certificates are used beyond those in the +certificate chain provided by the +.Ar file +argument. +.It Fl o Ar staplefile +Specify an output filename where the DER encoded response from the +OCSP server will be written, if the OCSP response validates. +A filename +of +.Ar - +will write the response to standard output. By default the response +is not saved. +.It Fl N +Do not use a nonce value in the OCSP request, or validate that the +nonce was returned in the OCSP response. +By default a nonce is always used and validated. +The use of this flag is a security risk as it will allow OCSP +responses to be replayed. +It should not be used unless the OCSP server does not support the +use of OCSP nonces. +.It Fl v +Increase verbosity. +This flag may be specified multiple times to get more verbose output. +The default behaviour is to be silent unless something goes wrong. +.Sh EXIT STATUS +.Nm +exits 0 if the OCSP response validates for the +certificate in +.Ar file +and all output is successfully written out. +Otherwise +.Nm +will exit >0. +.Sh SEE ALSO +.Xr httpd 8 , +.Xr nc 1 , +.Xr tls_config_set_ocsp_staple_mem 3 , +.Xr tls_config_set_ocsp_staple_file 3 , +.Sh BUGS +.Nm +will create the output file if it does not exit. +On failure a newly created output file will not be removed. +.Sh CAVEATS +While +.Nm +could possibly be used in scripts to query responders for server +certificates seen on client connections, this is almost always a bad +idea. +God kills a kitten every time you make an OCSP query from the +client side of a TLS connection. +.Sh AUTHORS +.Nm +was written by +.An Bob Beck 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 @@ +/* + * Copyright (c) 2017 Bob Beck + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "http.h" + +#define MAXAGE_SEC (14*24*60*60) +#define JITTER_SEC (60) + +typedef struct ocsp_request { + STACK_OF(X509) *fullchain; + OCSP_REQUEST * req; + char *url; + unsigned char *data; + size_t size; + int nonce; +} ocsp_request; + +int verbose; +#define vspew(fmt, ...) \ + do { if (verbose >= 1) fprintf(stderr, fmt, __VA_ARGS__); } while (0) +#define dspew(fmt, ...) \ + do { if (verbose >= 2) fprintf(stderr, fmt, __VA_ARGS__); } while (0) + +#define MAX_SERVERS_DNS 8 + +struct addr { + int family; /* 4 for PF_INET, 6 for PF_INET6 */ + char ip[INET6_ADDRSTRLEN]; +}; + +static ssize_t +host_dns(const char *s, struct addr vec[MAX_SERVERS_DNS]) +{ + struct addrinfo hints, *res0, *res; + int error; + ssize_t vecsz; + struct sockaddr *sa; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; /* DUMMY */ + /* ntpd MUST NOT use AI_ADDRCONFIG here */ + + error = getaddrinfo(s, NULL, &hints, &res0); + + if (error == EAI_AGAIN || + error == EAI_NODATA || + error == EAI_NONAME) + return(0); + + if (error) { + warnx("%s: parse error: %s", + s, gai_strerror(error)); + return(-1); + } + + for (vecsz = 0, res = res0; + NULL != res && vecsz < MAX_SERVERS_DNS; + res = res->ai_next) { + if (res->ai_family != AF_INET && + res->ai_family != AF_INET6) + continue; + + sa = res->ai_addr; + + if (AF_INET == res->ai_family) { + vec[vecsz].family = 4; + inet_ntop(AF_INET, + &(((struct sockaddr_in *)sa)->sin_addr), + vec[vecsz].ip, INET6_ADDRSTRLEN); + } else { + vec[vecsz].family = 6; + inet_ntop(AF_INET6, + &(((struct sockaddr_in6 *)sa)->sin6_addr), + vec[vecsz].ip, INET6_ADDRSTRLEN); + } + + dspew("DNS returns %s for %s\n", vec[vecsz].ip, s); + vecsz++; + break; + } + + freeaddrinfo(res0); + return(vecsz); +} + +/* + * Extract the domain and port from a URL. + * The url must be formatted as schema://address[/stuff]. + * This returns NULL on failure. + */ +static char * +url2host(const char *host, short *port, char **path) +{ + char *url, *ep; + + /* We only understand HTTP and HTTPS. */ + + if (0 == strncmp(host, "https://", 8)) { + *port = 443; + if (NULL == (url = strdup(host + 8))) { + warn("strdup"); + return (NULL); + } + } else if (0 == strncmp(host, "http://", 7)) { + *port = 80; + if (NULL == (url = strdup(host + 7))) { + warn("strdup"); + return (NULL); + } + } else { + warnx("%s: unknown schema", host); + return (NULL); + } + + /* Terminate path part. */ + + if (NULL != (ep = strchr(url, '/'))) { + *path = strdup(ep); + *ep = '\0'; + } else + *path = strdup(""); + + if (NULL == *path) { + warn("strdup"); + free(url); + return (NULL); + } + + return (url); +} + +static time_t +parse_ocsp_time(ASN1_GENERALIZEDTIME *gt) +{ + struct tm tm; + time_t rv = -1; + + if (gt == NULL) + return -1; + /* RFC 6960 specifies that all times in OCSP must be GENERALIZEDTIME */ + if (ASN1_time_parse(gt->data, gt->length, &tm, + V_ASN1_GENERALIZEDTIME) == -1) + return -1; + if ((rv = timegm(&tm)) == -1) + return -1; + return rv; +} + +static X509_STORE * +read_cacerts(char *file) +{ + X509_STORE *store; + X509_LOOKUP *lookup; + + if ((store = X509_STORE_new()) == NULL) { + warnx("Malloc failed"); + goto end; + } + if ((lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file())) == + NULL) { + warnx("Unable to load CA certs from file %s\n", file); + goto end; + } + if (file) { + if (!X509_LOOKUP_load_file(lookup, file, X509_FILETYPE_PEM)) { + warnx("Unable to load CA certs from file %s\n", file); + goto end; + } + } else + X509_LOOKUP_load_file(lookup, NULL, X509_FILETYPE_DEFAULT); + + if ((lookup = X509_STORE_add_lookup(store, X509_LOOKUP_hash_dir())) == + NULL) { + warnx("Unable to load CA certs from file %s\n", file); + goto end; + } + X509_LOOKUP_add_dir(lookup, NULL, X509_FILETYPE_DEFAULT); + ERR_clear_error(); + return store; + +end: + X509_STORE_free(store); + return NULL; +} + +static STACK_OF(X509) * +read_fullchain(const char *file, int *count) +{ + int i; + BIO *bio; + STACK_OF(X509_INFO) *xis = NULL; + X509_INFO *xi; + STACK_OF(X509) *rv = NULL; + + *count = 0; + + if ((bio = BIO_new_file(file, "r")) == NULL) { + warnx("Error opening %s\n", file); + ERR_print_errors_fp(stderr); + return NULL; + } + if ((xis = PEM_X509_INFO_read_bio(bio, NULL, NULL, NULL)) == NULL) { + warnx("Unable to read PEM format from %s\n", file); + ERR_print_errors_fp(stderr); + return NULL; + } + BIO_free(bio); + + if (sk_X509_INFO_num(xis) <= 0) { + warnx("No certificates in file %s\n", file); + goto end; + } + if ((rv = sk_X509_new_null()) == NULL) { + ERR_print_errors_fp(stderr); + goto end; + } + + for (i = 0; i < sk_X509_INFO_num(xis); i++) { + xi = sk_X509_INFO_value(xis, i); + if (xi->x509 == NULL) + continue; + if (!sk_X509_push(rv, xi->x509)) { + ERR_print_errors_fp(stderr); + sk_X509_pop_free(rv, X509_free); + rv = NULL; + goto end; + } + xi->x509 = NULL; + (*count)++; + } +end: + sk_X509_INFO_pop_free(xis, X509_INFO_free); + return rv; +} + +static inline X509 * +cert_from_chain(STACK_OF(X509) *fullchain) +{ + return sk_X509_value(fullchain, 0); +} + +static X509 * +issuer_from_chain(STACK_OF(X509) *fullchain) +{ + X509 *cert, *issuer; + X509_NAME *issuer_name; + + cert = cert_from_chain(fullchain); + if ((issuer_name = X509_get_issuer_name(cert)) == NULL) + return NULL; + + issuer = X509_find_by_subject(fullchain, issuer_name); + return issuer; +} + +static ocsp_request * +ocsp_request_new_from_cert(char *file, int nonce) +{ + X509 *cert = NULL; + int count = 0; + OCSP_CERTID *id; + ocsp_request *request; + const EVP_MD *cert_id_md = NULL; + X509 *issuer = NULL; + STACK_OF(OPENSSL_STRING) *urls; + + if ((request = calloc(1, sizeof(ocsp_request))) == NULL) { + warn("malloc"); + return NULL; + } + + if ((request->req = OCSP_REQUEST_new()) == NULL) + return NULL; + + request->fullchain = read_fullchain(file, &count); + /* Drop rpath from pledge, we don't need to read anymore */ + if (pledge("stdio inet dns", NULL) == -1) + err(EXIT_FAILURE, "pledge"); + + if (request->fullchain == NULL) + return NULL; + if (count <= 1) { + warnx("File %s does not contain a cert chain", + file); + return NULL; + } + if ((cert = cert_from_chain(request->fullchain)) == NULL) { + warnx("No certificate found in %s", file); + return NULL; + } + if ((issuer = issuer_from_chain(request->fullchain)) == NULL) { + warnx("Unable to find issuer for cert in %s", file); + return NULL; + } + + urls = X509_get1_ocsp(cert); + if (urls == NULL || sk_OPENSSL_STRING_num(urls) <= 0) { + warnx("Certificate in %s contains no OCSP url", file); + return NULL; + } + if ((request->url = strdup(sk_OPENSSL_STRING_value(urls, 0))) == NULL) + return NULL; + X509_email_free(urls); + + cert_id_md = EVP_sha1(); /* XXX. This sucks but OCSP is poopy */ + if ((id = OCSP_cert_to_id(cert_id_md, cert, issuer)) == NULL) { + warnx("Unable to get certificate id from cert in %s", file); + ERR_print_errors_fp(stderr); + return NULL; + } + if (OCSP_request_add0_id(request->req, id) == NULL) { + warnx("Unable to add certificate id to request"); + ERR_print_errors_fp(stderr); + return NULL; + } + + request->nonce = nonce; + if (request->nonce) + OCSP_request_add1_nonce(request->req, NULL, -1); + + if ((request->size = i2d_OCSP_REQUEST(request->req, + &request->data)) <= 0) { + warnx("Unable to encode ocsp request"); + return NULL; + } + if (request->data == NULL) { + warnx("Unable to allocte memory"); + return NULL; + } + return(request); +} + + +int +validate_response(char *buf, size_t size, ocsp_request *request, + X509_STORE *store, char *host, char *file) +{ + ASN1_GENERALIZEDTIME *revtime = NULL, *thisupd = NULL, *nextupd = NULL; + const unsigned char **p = (const unsigned char **)&buf; + int status, cert_status=0, crl_reason=0; + time_t now, rev_t = -1, this_t, next_t; + OCSP_RESPONSE *resp; + OCSP_BASICRESP *bresp; + OCSP_CERTID *cid; + X509 *cert, *issuer; + + if ((cert = cert_from_chain(request->fullchain)) == NULL) { + warnx("No certificate found in %s", file); + return 0; + } + if ((issuer = issuer_from_chain(request->fullchain)) == NULL) { + warnx("Unable to find certificate issuer for cert in %s", + file); + return 0; + } + if ((cid = OCSP_cert_to_id(NULL, cert, issuer)) == NULL) { + warnx("Unable to get issuer cert/CID in %s", file); + return(0); + } + + if ((resp = d2i_OCSP_RESPONSE(NULL, p, size)) == NULL) { + warnx("OCSP response unserializable from host %s", host); + return 0; + } + + if ((bresp = OCSP_response_get1_basic(resp)) == NULL) { + warnx("Failed to load OCSP response from %s", host); + return(0); + } + + if (OCSP_basic_verify(bresp, request->fullchain, store, + OCSP_TRUSTOTHER) != 1) { + ERR_print_errors_fp(stderr); + warnx("OCSP verify failed from %s", host); + return 0; + } + dspew("OCSP response signature validated from %s\n", host); + + status = OCSP_response_status(resp); + if (status != OCSP_RESPONSE_STATUS_SUCCESSFUL) { + warnx("OCSP Failure: code %d (%s) from host %s", + status, OCSP_response_status_str(status), host); + return(0); + } + dspew("OCSP response status %d from host %s\n", status, host); + + /* Check the nonce if we sent one */ + + if (request->nonce) { + if (OCSP_check_nonce(request->req, bresp) <= 0) { + warnx("No OCSP nonce, or mismatch, from host %s", host); + return 0; + } + } + + if (OCSP_resp_find_status(bresp, cid, &cert_status, &crl_reason, + &revtime, &thisupd, &nextupd) != 1) { + warnx("OCSP verify failed: no result for cert"); + return 0; + } + + if (revtime && (rev_t = parse_ocsp_time(revtime)) == -1) { + warnx("Unable to parse revocation time in OCSP reply"); + return 0; + } + /* + * Belt and suspenders, Treat it as revoked if there is either + * a revocation time, or status revoked. + */ + if (rev_t != -1 || cert_status == V_OCSP_CERTSTATUS_REVOKED) { + warnx("Invalid OCSP reply: certificate is revoked"); + if (rev_t != -1) + warnx("Certificate revoked at: %s", ctime(&rev_t)); + return 0; + } + if ((this_t = parse_ocsp_time(thisupd)) == -1) { + warnx("unable to parse this update time in OCSP reply"); + return 0; + } + if ((next_t = parse_ocsp_time(nextupd)) == -1) { + warnx("unable to parse next update time in OCSP reply"); + return 0; + } + + /* Don't allow this update to precede next update */ + if (this_t >= next_t) { + warnx("Invalid OCSP reply: this update >= next update"); + return 0; + } + + now = time(NULL); + /* + * Check that this update is not more than JITTER seconds + * in the future. + */ + if (this_t > now + JITTER_SEC) { + warnx("Invalid OCSP reply: this update is in the future (%s)", + ctime(&this_t)); + return 0; + } + + /* + * Check that this update is not more than MAXSEC + * in the past. + */ + if (this_t < now - MAXAGE_SEC) { + warnx("Invalid OCSP reply: this update is too old (%s)", + ctime(&this_t)); + return 0; + } + + /* + * Check that next update is still valid + */ + if (next_t < now - JITTER_SEC) { + warnx("Invalid OCSP reply: reply has expired (%s)", + ctime(&next_t)); + return 0; + } + + vspew("OCSP response validated from %s\n", host); + vspew(" This Update: %s", ctime(&this_t)); + vspew(" Next Update: %s", ctime(&next_t)); + return 1; +} + +static void +usage(void) +{ + errx(1, "Usage: %s [-N] [-v] [-o staplefile] certfile", getprogname()); +} + +int +main (int argc, char **argv) +{ + char *host = NULL, *path = "/", *certfile = NULL, *outfile = NULL, + *cafile = NULL; + struct addr addrs[MAX_SERVERS_DNS] = { }; + struct source sources[MAX_SERVERS_DNS]; + int i, ch, staplefd = -1, nonce = 1; + ocsp_request *request = NULL; + size_t rescount, httphsz; + struct httphead *httph; + struct httpget *hget; + X509_STORE *castore; + ssize_t written, w; + short port; + + while ((ch = getopt(argc, argv, "C:No:v")) != -1) { + switch (ch) { + case 'C': + cafile = optarg; + break; + case 'N': + nonce = 0; + break; + case 'o': + outfile = optarg; + break; + case 'v': + verbose++; + break; + default: + usage(); + } + } + argc -= optind; + argv += optind; + + if ((certfile = argv[0]) == NULL) + usage(); + + if (outfile != NULL) { + if (strcmp(outfile, "-") == 0) + staplefd = STDOUT_FILENO; + else + staplefd = open(outfile, O_WRONLY|O_CREAT); + if (staplefd < 0) + err(EXIT_FAILURE, "Unable to open output file %s", + outfile); + } + + if (pledge("stdio inet rpath dns", NULL) == -1) + err(EXIT_FAILURE, "pledge"); + + /* + * Load our certificate and keystore, and build up an + * OSCP request based on the full certificate chain + * we have been given to check. + */ + if ((castore = read_cacerts(NULL)) == NULL) + errx(EXIT_FAILURE, "Unable to load %s", cafile); + + if ((request = ocsp_request_new_from_cert(certfile, nonce)) == NULL) + errx(EXIT_FAILURE, "Unable to build OCSP request"); + + if ((host = url2host(request->url, &port, &path)) == NULL) + errx(EXIT_FAILURE, "Invalid OCSP url %s from %s", request->url, + certfile); + dspew("Built an %ld byte ocsp request\n", request->size); + vspew("Using %s to host %s, port %d, path %s\n", + port == 443 ? "https" : "http", host, port, path); + + rescount = host_dns(host, addrs); + for (i = 0; i < rescount; i++) { + sources[i].ip = addrs[i].ip; + sources[i].family = addrs[i].family; + } + + /* + * Do an HTTP post to send our request to the OCSP + * server, and hopefully get an answer back + */ + hget = http_get(sources, rescount, host, port, path, + request->data, request->size); + if (hget == NULL) + errx(EXIT_FAILURE, "http_get"); + httph = http_head_parse(hget->http, hget->xfer, &httphsz); + dspew("Server at %s returns:\n", host); + for (i = 0; i < httphsz; i++) + dspew(" [%s]=[%s]\n", httph[i].key, httph[i].val); + dspew(" [Body]=[%ld bytes]\n", hget->bodypartsz); + if (hget->bodypartsz <= 0) + errx(EXIT_FAILURE, "No body in reply from %s", host); + + /* + * Pledge minimally before fiddling with libcrypto init routines + * and untrusted input from someone's OCSP server. + */ + + if (pledge("stdio", NULL) == -1) + err(EXIT_FAILURE, "pledge"); + + /* + * Validate the OCSP response we got back + */ + ERR_load_crypto_strings(); + OPENSSL_add_all_algorithms_noconf(); + if (!validate_response(hget->bodypart, hget->bodypartsz, + request, castore, host, certfile)) + errx(EXIT_FAILURE, "Can not validate %s", certfile); + + /* + * If we have been given a place to save a staple, + * write out the DER format response to the staplefd + */ + if (staplefd >= 0) { + ftruncate(staplefd, 0); + w = 0 ; + written = 0; + while (written < hget->bodypartsz) { + w = write(staplefd, hget->bodypart + written, + hget->bodypartsz - written); + if (w == -1) { + if (errno != EINTR && errno != EAGAIN) + err(1, "Write of OCSP response failed"); + } else + written += w; + } + close(staplefd); + } + exit(EXIT_SUCCESS); +} -- cgit v1.2.3-55-g6feb