/* $OpenBSD: ocsp.c,v 1.25 2024/01/17 08:25:02 claudio Exp $ */ /* * Copyright (c) 2014 Markus Friedl * Copyright (c) 2005 Marco Pfatschbacher * * 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. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "iked.h" struct iked_ocsp { struct iked *ocsp_env; /* back pointer to env */ struct iked_sahdr ocsp_sh; /* ike sa */ uint8_t ocsp_type; /* auth type */ struct iked_socket *ocsp_sock; /* socket to ocsp responder */ BIO *ocsp_cbio; /* matching OpenSSL obj */ OCSP_CERTID *ocsp_id; /* ocsp-id for cert */ OCSP_REQUEST *ocsp_req; /* ocsp-request */ OCSP_REQ_CTX *ocsp_req_ctx; /* async ocsp-request */ }; struct ocsp_connect { struct iked_sahdr oc_sh; struct iked_socket oc_sock; char *oc_path; char *oc_url; }; #define OCSP_TIMEOUT 30 /* priv */ void ocsp_connect_cb(int, short, void *); int ocsp_connect_finish(struct iked *, int, struct ocsp_connect *); /* unpriv */ void ocsp_free(struct iked_ocsp *); void ocsp_callback(int, short, void *); void ocsp_parse_response(struct iked_ocsp *, OCSP_RESPONSE *); STACK_OF(X509) *ocsp_load_certs(const char *); int ocsp_validate_finish(struct iked_ocsp *, int); /* priv */ /* async connect to configure ocsp-responder */ int ocsp_connect(struct iked *env, struct imsg *imsg) { struct ocsp_connect *oc = NULL; struct iked_sahdr sh; struct addrinfo hints, *res0 = NULL, *res; struct timeval tv; uint8_t *ptr; size_t len; char *host = NULL, *port = NULL, *path = NULL; char *url, *freeme = NULL; int use_ssl, fd = -1, ret = -1, error; IMSG_SIZE_CHECK(imsg, &sh); ptr = (uint8_t *)imsg->data; len = IMSG_DATA_SIZE(imsg); memcpy(&sh, ptr, sizeof(sh)); ptr += sizeof(sh); len -= sizeof(sh); if (len > 0) url = freeme = get_string(ptr, len); else if (env->sc_ocsp_url) url = env->sc_ocsp_url; else { log_warnx("%s: no ocsp url", SPI_SH(&sh, __func__)); goto done; } if (!OCSP_parse_url(url, &host, &port, &path, &use_ssl)) { log_warnx("%s: error parsing OCSP-request-URL: %s", SPI_SH(&sh, __func__), url); goto done; } if (use_ssl) { log_warnx("%s: OCSP over SSL not supported: %s", SPI_SH(&sh, __func__), url); goto done; } if ((fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0)) == -1) { log_debug("%s: socket failed", SPI_SH(&sh, __func__)); goto done; } if ((oc = calloc(1, sizeof(*oc))) == NULL) { log_debug("%s: calloc failed", __func__); goto done; } bzero(&hints, sizeof(struct addrinfo)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; error = getaddrinfo(host, port, &hints, &res0); if (error) { log_warn("%s: getaddrinfo(%s, %s) failed", SPI_SH(&sh, __func__), host, port); goto done; } /* XXX just pick the first answer. we could loop instead */ for (res = res0; res; res = res->ai_next) if (res->ai_family == AF_INET) break; if (res == NULL) { log_debug("%s: no addr to connect to for %s:%s", SPI_SH(&sh, __func__), host, port); goto done; } oc->oc_sock.sock_fd = fd; oc->oc_sock.sock_env = env; oc->oc_sh = sh; oc->oc_path = path; oc->oc_url = strdup(url); if (oc->oc_url == NULL) { log_warn("%s: strdup failed", SPI_SH(&sh, __func__)); goto done; } path = NULL; log_debug("%s: connect(%s, %s)", __func__, host, port); if (connect(fd, res->ai_addr, res->ai_addrlen) == -1) { /* register callback for ansync connect */ if (errno == EINPROGRESS) { tv.tv_sec = OCSP_TIMEOUT; tv.tv_usec = 0; event_set(&oc->oc_sock.sock_ev, fd, EV_WRITE, ocsp_connect_cb, oc); event_add(&oc->oc_sock.sock_ev, &tv); ret = 0; } else log_warn("%s: connect(%s, %s)", SPI_SH(&oc->oc_sh, __func__), host, port); } else { ocsp_connect_finish(env, fd, oc); ret = 0; } done: if (res0) freeaddrinfo(res0); free(freeme); free(host); free(port); free(path); if (ret == -1) { ocsp_connect_finish(env, -1, oc); if (fd >= 0) close(fd); } return (ret); } /* callback triggered if connection to ocsp-responder completes/fails */ void ocsp_connect_cb(int fd, short event, void *arg) { struct ocsp_connect *oc = arg; int error, send_fd = -1; socklen_t len; if (event == EV_TIMEOUT) { log_info("%s: timeout, giving up", SPI_SH(&oc->oc_sh, __func__)); goto done; } len = sizeof(error); if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len) == -1) { log_warn("%s: getsockopt SOL_SOCKET SO_ERROR", SPI_SH(&oc->oc_sh, __func__)); } else if (error) { log_warnx("%s: error while connecting: %s", SPI_SH(&oc->oc_sh, __func__), strerror(error)); } else { send_fd = fd; } done: ocsp_connect_finish(oc->oc_sock.sock_env, send_fd, oc); /* if we did not send the fd, we need to close it ourself */ if (send_fd == -1) close(fd); } /* send FD+path or error back to CA process */ int ocsp_connect_finish(struct iked *env, int fd, struct ocsp_connect *oc) { struct iovec iov[2]; int iovcnt = 0, ret; iov[iovcnt].iov_base = &oc->oc_sh; iov[iovcnt].iov_len = sizeof(oc->oc_sh); iovcnt++; if (oc && fd >= 0) { /* the imsg framework will close the FD after send */ iov[iovcnt].iov_base = oc->oc_path; iov[iovcnt].iov_len = strlen(oc->oc_path); iovcnt++; ret = proc_composev_imsg(&env->sc_ps, PROC_CERT, -1, IMSG_OCSP_FD, -1, fd, iov, iovcnt); } else { if (oc) log_info("%s: connect failed for %s", SPI_SH(&oc->oc_sh, __func__), oc->oc_url ? oc->oc_url : "unknown"); else log_info("%s: connect failed", __func__); ret = proc_composev_imsg(&env->sc_ps, PROC_CERT, -1, IMSG_OCSP_FD, -1, -1, iov, iovcnt); if (fd >= 0) close(fd); } if (oc) { free(oc->oc_url); free(oc->oc_path); free(oc); } return (ret); } /* unpriv */ /* validate the certifcate stored in 'data' by querying the ocsp-responder */ int ocsp_validate_cert(struct iked *env, void *data, size_t len, struct iked_sahdr sh, uint8_t type, X509 *issuer) { struct iovec iov[2]; STACK_OF(OPENSSL_STRING) *aia; /* Authority Information Access */ struct iked_ocsp_entry *ioe; struct iked_ocsp *ocsp; OCSP_CERTID *id = NULL; char *url; BIO *rawcert = NULL; X509 *cert = NULL; int ret, iovcnt = 0; if (issuer == NULL) return (-1); if ((ioe = calloc(1, sizeof(*ioe))) == NULL) return (-1); if ((ocsp = calloc(1, sizeof(*ocsp))) == NULL) { free(ioe); return (-1); } ocsp->ocsp_env = env; ocsp->ocsp_sh = sh; ocsp->ocsp_type = type; if ((rawcert = BIO_new_mem_buf(data, len)) == NULL || (cert = d2i_X509_bio(rawcert, NULL)) == NULL || (ocsp->ocsp_cbio = BIO_new(BIO_s_socket())) == NULL || (ocsp->ocsp_req = OCSP_REQUEST_new()) == NULL || (id = OCSP_cert_to_id(NULL, cert, issuer)) == NULL || !OCSP_request_add0_id(ocsp->ocsp_req, id)) goto err; /* id is owned by and freed together with ocsp_req */ ocsp->ocsp_id = id; BIO_free(rawcert); X509_free(cert); ioe->ioe_ocsp = ocsp; TAILQ_INSERT_TAIL(&env->sc_ocsp, ioe, ioe_entry); /* pass SA header */ iov[iovcnt].iov_base = &ocsp->ocsp_sh; iov[iovcnt].iov_len = sizeof(ocsp->ocsp_sh); iovcnt++; /* pass optional ocsp-url from issuer */ if ((aia = X509_get1_ocsp(issuer)) != NULL) { url = sk_OPENSSL_STRING_value(aia, 0); log_debug("%s: aia %s", __func__, url); iov[iovcnt].iov_base = url; iov[iovcnt].iov_len = strlen(url); iovcnt++; } /* request connection to ocsp-responder */ ret = proc_composev(&env->sc_ps, PROC_PARENT, IMSG_OCSP_FD, iov, iovcnt); X509_email_free(aia); /* free stack of openssl strings */ return (ret); err: ca_sslerror(__func__); free(ioe); BIO_free(rawcert); X509_free(cert); OCSP_CERTID_free(id); ocsp_validate_finish(ocsp, 0); /* failed */ return (-1); } /* free ocsp query context */ void ocsp_free(struct iked_ocsp *ocsp) { if (ocsp != NULL) { if (ocsp->ocsp_sock != NULL) { close(ocsp->ocsp_sock->sock_fd); free(ocsp->ocsp_sock); } BIO_free_all(ocsp->ocsp_cbio); OCSP_REQ_CTX_free(ocsp->ocsp_req_ctx); OCSP_REQUEST_free(ocsp->ocsp_req); free(ocsp); } } /* we got a connection to the ocsp responder */ int ocsp_receive_fd(struct iked *env, struct imsg *imsg) { struct iked_ocsp_entry *ioe = NULL; struct iked_ocsp *ocsp = NULL, *ocsp_tmp; struct iked_socket *sock; struct iked_sahdr sh; struct timeval tv; uint8_t *ptr; char *path = NULL; size_t len; int fd, ret = -1; IMSG_SIZE_CHECK(imsg, &sh); ptr = (uint8_t *)imsg->data; len = IMSG_DATA_SIZE(imsg); memcpy(&sh, ptr, sizeof(sh)); ptr += sizeof(sh); len -= sizeof(sh); TAILQ_FOREACH(ioe, &env->sc_ocsp, ioe_entry) { ocsp_tmp = ioe->ioe_ocsp; if (memcmp(&ocsp_tmp->ocsp_sh, &sh, sizeof(sh)) == 0) break; } if (ioe == NULL) { log_debug("%s: no pending request found", __func__); if ((fd = imsg_get_fd(imsg)) != -1) /* XXX */ close(fd); return (-1); } TAILQ_REMOVE(&env->sc_ocsp, ioe, ioe_entry); ocsp = ioe->ioe_ocsp; free(ioe); if ((fd = imsg_get_fd(imsg)) == -1) goto done; if ((sock = calloc(1, sizeof(*sock))) == NULL) fatal("ocsp_receive_fd: calloc sock"); /* note that sock_addr is not set */ sock->sock_fd = fd; sock->sock_env = env; ocsp->ocsp_sock = sock; log_debug("%s: received socket fd %d", __func__, sock->sock_fd); /* fetch 'path' and 'fd' from imsg */ if ((path = get_string(ptr, len)) == NULL) goto done; BIO_set_fd(ocsp->ocsp_cbio, sock->sock_fd, BIO_NOCLOSE); if ((ocsp->ocsp_req_ctx = OCSP_sendreq_new(ocsp->ocsp_cbio, path, NULL, -1)) == NULL) goto done; if (!OCSP_REQ_CTX_set1_req(ocsp->ocsp_req_ctx, ocsp->ocsp_req)) goto done; tv.tv_sec = OCSP_TIMEOUT; tv.tv_usec = 0; event_set(&sock->sock_ev, sock->sock_fd, EV_WRITE, ocsp_callback, ocsp); event_add(&sock->sock_ev, &tv); ret = 0; done: if (ret == -1) ocsp_validate_finish(ocsp, 0); /* failed */ free(path); return (ret); } /* load a stack of x509 certificates */ STACK_OF(X509)* ocsp_load_certs(const char *file) { BIO *bio = NULL; STACK_OF(X509) *certs = NULL; STACK_OF(X509_INFO) *xis = NULL; X509_INFO *xi; int i; if ((bio = BIO_new_file(file, "r")) == NULL) { log_warn("%s: BIO_new_file failed for %s", __func__, file); return (NULL); } if ((xis = PEM_X509_INFO_read_bio(bio, NULL, NULL, NULL)) == NULL) { ca_sslerror(__func__); goto done; } if ((certs = sk_X509_new_null()) == NULL) { log_debug("%s: sk_X509_new_null failed for %s", __func__, file); goto done; } for (i = 0; i < sk_X509_INFO_num(xis); i++) { xi = sk_X509_INFO_value(xis, i); if (xi->x509) { if (!sk_X509_push(certs, xi->x509)) goto done; xi->x509 = NULL; } } done: BIO_free(bio); sk_X509_INFO_pop_free(xis, X509_INFO_free); if (sk_X509_num(certs) <= 0) { sk_X509_free(certs); certs = NULL; } return (certs); } /* read/write callback that sends the requests and reads the ocsp response */ void ocsp_callback(int fd, short event, void *arg) { struct iked_ocsp *ocsp = arg; struct iked_socket *sock = ocsp->ocsp_sock; struct timeval tv; OCSP_RESPONSE *resp = NULL; if (event == EV_TIMEOUT) { log_info("%s: timeout, giving up", SPI_SH(&ocsp->ocsp_sh, __func__)); ocsp_validate_finish(ocsp, 0); return; } /* * Only call OCSP_sendreq_nbio() if should_read/write is * either not requested or read/write can be called. */ if ((!BIO_should_read(ocsp->ocsp_cbio) || (event & EV_READ)) && (!BIO_should_write(ocsp->ocsp_cbio) || (event & EV_WRITE)) && OCSP_sendreq_nbio(&resp, ocsp->ocsp_req_ctx) != -1 ) { ocsp_parse_response(ocsp, resp); return; } if (BIO_should_read(ocsp->ocsp_cbio)) event_set(&sock->sock_ev, sock->sock_fd, EV_READ, ocsp_callback, ocsp); else if (BIO_should_write(ocsp->ocsp_cbio)) event_set(&sock->sock_ev, sock->sock_fd, EV_WRITE, ocsp_callback, ocsp); tv.tv_sec = OCSP_TIMEOUT; tv.tv_usec = 0; event_add(&sock->sock_ev, &tv); } /* parse the actual OCSP response */ void ocsp_parse_response(struct iked_ocsp *ocsp, OCSP_RESPONSE *resp) { struct iked *env = ocsp->ocsp_env; X509_STORE *store = NULL; STACK_OF(X509) *verify_other = NULL; OCSP_BASICRESP *bs = NULL; ASN1_GENERALIZEDTIME *rev, *thisupd, *nextupd; const char *errstr; int reason = 0, valid = 0, verify_flags = 0; int status; if (!resp) { errstr = "error querying OCSP responder"; goto done; } status = OCSP_response_status(resp); if (status != OCSP_RESPONSE_STATUS_SUCCESSFUL) { errstr = OCSP_response_status_str(status); goto done; } verify_other = ocsp_load_certs(IKED_OCSP_RESPCERT); verify_flags |= OCSP_TRUSTOTHER; if (!verify_other) { errstr = "no verify_other"; goto done; } bs = OCSP_response_get1_basic(resp); if (!bs) { errstr = "error parsing response"; goto done; } status = OCSP_check_nonce(ocsp->ocsp_req, bs); if (status <= 0) { if (status == -1) log_warnx("%s: no nonce in response", SPI_SH(&ocsp->ocsp_sh, __func__)); else { errstr = "nonce verify error"; goto done; } } store = X509_STORE_new(); status = OCSP_basic_verify(bs, verify_other, store, verify_flags); if (status < 0) status = OCSP_basic_verify(bs, NULL, store, 0); if (status <= 0) { ca_sslerror(__func__); errstr = "response verify failure"; goto done; } log_debug("%s: response verify ok", SPI_SH(&ocsp->ocsp_sh, __func__)); if (!OCSP_resp_find_status(bs, ocsp->ocsp_id, &status, &reason, &rev, &thisupd, &nextupd)) { errstr = "no status found"; goto done; } if (env->sc_ocsp_tolerate && !OCSP_check_validity(thisupd, nextupd, env->sc_ocsp_tolerate, env->sc_ocsp_maxage)) { ca_sslerror(SPI_SH(&ocsp->ocsp_sh, __func__)); errstr = "status times invalid"; goto done; } errstr = OCSP_cert_status_str(status); if (status == V_OCSP_CERTSTATUS_GOOD) { log_debug("%s: status: %s", SPI_SH(&ocsp->ocsp_sh, __func__), errstr); valid = 1; } done: if (!valid) { log_debug("%s: status: %s", __func__, errstr); } X509_STORE_free(store); sk_X509_pop_free(verify_other, X509_free); OCSP_RESPONSE_free(resp); OCSP_BASICRESP_free(bs); ocsp_validate_finish(ocsp, valid); } /* * finish the ocsp_validate_cert() RPC by sending the appropriate * message back to the IKEv2 process */ int ocsp_validate_finish(struct iked_ocsp *ocsp, int valid) { struct iked *env = ocsp->ocsp_env; struct iovec iov[2]; int iovcnt = 2, ret, cmd; iov[0].iov_base = &ocsp->ocsp_sh; iov[0].iov_len = sizeof(ocsp->ocsp_sh); iov[1].iov_base = &ocsp->ocsp_type; iov[1].iov_len = sizeof(ocsp->ocsp_type); cmd = valid ? IMSG_CERTVALID : IMSG_CERTINVALID; ret = proc_composev(&env->sc_ps, PROC_IKEV2, cmd, iov, iovcnt); ocsp_free(ocsp); return (ret); }