Path: blob/main/crypto/krb5/src/plugins/tls/k5tls/openssl.c
34914 views
/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */1/* plugins/tls/k5tls/openssl.c - OpenSSL TLS module implementation */2/*3* Copyright 2013,2014 Red Hat, Inc.4*5* Redistribution and use in source and binary forms, with or without6* modification, are permitted provided that the following conditions are met:7*8* 1. Redistributions of source code must retain the above copyright9* notice, this list of conditions and the following disclaimer.10*11* 2. Redistributions in binary form must reproduce the above copyright12* notice, this list of conditions and the following disclaimer in13* the documentation and/or other materials provided with the14* distribution.15*16* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS17* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED18* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A19* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER20* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,21* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,22* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR23* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF24* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING25* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS26* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.27*/2829#include "k5-int.h"30#include "k5-utf8.h"31#include "k5-tls.h"3233#ifdef TLS_IMPL_OPENSSL34#include <openssl/err.h>35#include <openssl/ssl.h>36#include <openssl/x509.h>37#include <openssl/x509v3.h>38#include <dirent.h>3940struct k5_tls_handle_st {41SSL *ssl;42char *servername;43};4445static int ex_context_id = -1;46static int ex_handle_id = -1;4748MAKE_INIT_FUNCTION(init_openssl);4950int51init_openssl(void)52{53SSL_library_init();54SSL_load_error_strings();55OpenSSL_add_all_algorithms();56ex_context_id = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL);57ex_handle_id = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL);58return 0;59}6061static void62flush_errors(krb5_context context)63{64unsigned long err;65char buf[128];6667while ((err = ERR_get_error()) != 0) {68ERR_error_string_n(err, buf, sizeof(buf));69TRACE_TLS_ERROR(context, buf);70}71}7273/* Return the passed-in character, lower-cased if it's an ASCII character. */74static inline char75ascii_tolower(char p)76{77if (KRB5_UPPER(p))78return p + ('a' - 'A');79return p;80}8182/*83* Check a single label. If allow_wildcard is true, and the presented name84* includes a wildcard, return true and note that we matched a wildcard.85* Otherwise, for both the presented and expected values, do a case-insensitive86* comparison of ASCII characters, and a case-sensitive comparison of87* everything else.88*/89static krb5_boolean90label_match(const char *presented, size_t plen, const char *expected,91size_t elen, krb5_boolean allow_wildcard, krb5_boolean *wildcard)92{93unsigned int i;9495if (allow_wildcard && plen == 1 && presented[0] == '*') {96*wildcard = TRUE;97return TRUE;98}99100if (plen != elen)101return FALSE;102103for (i = 0; i < elen; i++) {104if (ascii_tolower(presented[i]) != ascii_tolower(expected[i]))105return FALSE;106}107return TRUE;108}109110/* Break up the two names and check them, label by label. */111static krb5_boolean112domain_match(const char *presented, size_t plen, const char *expected)113{114const char *p, *q, *r, *s;115int n_label;116krb5_boolean used_wildcard = FALSE;117118n_label = 0;119p = presented;120r = expected;121while (p < presented + plen && *r != '\0') {122q = memchr(p, '.', plen - (p - presented));123if (q == NULL)124q = presented + plen;125s = r + strcspn(r, ".");126if (!label_match(p, q - p, r, s - r, n_label == 0, &used_wildcard))127return FALSE;128p = q < presented + plen ? q + 1 : q;129r = *s ? s + 1 : s;130n_label++;131}132if (used_wildcard && n_label <= 2)133return FALSE;134if (p == presented + plen && *r == '\0')135return TRUE;136return FALSE;137}138139/* Fetch the list of subjectAltNames from a certificate. */140static GENERAL_NAMES *141get_cert_sans(X509 *x)142{143int ext;144X509_EXTENSION *san_ext;145146ext = X509_get_ext_by_NID(x, NID_subject_alt_name, -1);147if (ext < 0)148return NULL;149san_ext = X509_get_ext(x, ext);150if (san_ext == NULL)151return NULL;152return X509V3_EXT_d2i(san_ext);153}154155/* Fetch a CN value from the subjct name field, returning its length, or -1 if156* there is no subject name or it contains no CN value. */157static int158get_cert_cn(X509 *x, char *buf, size_t bufsize)159{160X509_NAME *name;161162name = X509_get_subject_name(x);163if (name == NULL)164return -1;165return X509_NAME_get_text_by_NID(name, NID_commonName, buf, bufsize);166}167168/* Return true if text matches any of the addresses we can recover from x. */169static krb5_boolean170check_cert_address(X509 *x, const char *text)171{172char buf[1024];173GENERAL_NAMES *sans;174GENERAL_NAME *san = NULL;175ASN1_OCTET_STRING *ip;176krb5_boolean found_ip_san = FALSE, matched = FALSE;177int n_sans, i;178int name_length;179struct in_addr sin;180struct in6_addr sin6;181182/* Parse the IP address into an octet string. */183ip = ASN1_OCTET_STRING_new();184if (ip == NULL)185return FALSE;186if (inet_pton(AF_INET, text, &sin)) {187ASN1_OCTET_STRING_set(ip, (unsigned char *)&sin, sizeof(sin));188} else if (inet_pton(AF_INET6, text, &sin6)) {189ASN1_OCTET_STRING_set(ip, (unsigned char *)&sin6, sizeof(sin6));190} else {191ASN1_OCTET_STRING_free(ip);192return FALSE;193}194195/* Check for matches in ipaddress subjectAltName values. */196sans = get_cert_sans(x);197if (sans != NULL) {198n_sans = sk_GENERAL_NAME_num(sans);199for (i = 0; i < n_sans; i++) {200san = sk_GENERAL_NAME_value(sans, i);201if (san->type != GEN_IPADD)202continue;203found_ip_san = TRUE;204matched = (ASN1_OCTET_STRING_cmp(ip, san->d.iPAddress) == 0);205if (matched)206break;207}208sk_GENERAL_NAME_pop_free(sans, GENERAL_NAME_free);209}210ASN1_OCTET_STRING_free(ip);211212if (found_ip_san)213return matched;214215/* Check for a match against the CN value in the peer's subject name. */216name_length = get_cert_cn(x, buf, sizeof(buf));217if (name_length >= 0) {218/* Do a string compare to check if it's an acceptable value. */219return strlen(text) == (size_t)name_length &&220strncmp(text, buf, name_length) == 0;221}222223/* We didn't find a match. */224return FALSE;225}226227/* Return true if expected matches any of the names we can recover from x. */228static krb5_boolean229check_cert_servername(X509 *x, const char *expected)230{231char buf[1024];232GENERAL_NAMES *sans;233GENERAL_NAME *san = NULL;234unsigned char *dnsname;235krb5_boolean found_dns_san = FALSE, matched = FALSE;236int name_length, n_sans, i;237238/* Check for matches in dnsname subjectAltName values. */239sans = get_cert_sans(x);240if (sans != NULL) {241n_sans = sk_GENERAL_NAME_num(sans);242for (i = 0; i < n_sans; i++) {243san = sk_GENERAL_NAME_value(sans, i);244if (san->type != GEN_DNS)245continue;246found_dns_san = TRUE;247dnsname = NULL;248name_length = ASN1_STRING_to_UTF8(&dnsname, san->d.dNSName);249if (dnsname == NULL)250continue;251matched = domain_match((char *)dnsname, name_length, expected);252OPENSSL_free(dnsname);253if (matched)254break;255}256sk_GENERAL_NAME_pop_free(sans, GENERAL_NAME_free);257}258259if (matched)260return TRUE;261if (found_dns_san)262return matched;263264/* Check for a match against the CN value in the peer's subject name. */265name_length = get_cert_cn(x, buf, sizeof(buf));266if (name_length >= 0)267return domain_match(buf, name_length, expected);268269/* We didn't find a match. */270return FALSE;271}272273static krb5_boolean274check_cert_name_or_ip(X509 *x, const char *expected_name)275{276struct in_addr in;277struct in6_addr in6;278279if (inet_pton(AF_INET, expected_name, &in) != 0 ||280inet_pton(AF_INET6, expected_name, &in6) != 0) {281return check_cert_address(x, expected_name);282} else {283return check_cert_servername(x, expected_name);284}285}286287static int288verify_callback(int preverify_ok, X509_STORE_CTX *store_ctx)289{290X509 *x;291SSL *ssl;292BIO *bio;293krb5_context context;294int err, depth;295k5_tls_handle handle;296const char *cert = NULL, *errstr, *expected_name;297size_t count;298299ssl = X509_STORE_CTX_get_ex_data(store_ctx,300SSL_get_ex_data_X509_STORE_CTX_idx());301context = SSL_get_ex_data(ssl, ex_context_id);302handle = SSL_get_ex_data(ssl, ex_handle_id);303assert(context != NULL && handle != NULL);304/* We do have the peer's cert, right? */305x = X509_STORE_CTX_get_current_cert(store_ctx);306if (x == NULL) {307TRACE_TLS_NO_REMOTE_CERTIFICATE(context);308return 0;309}310/* Figure out where we are. */311depth = X509_STORE_CTX_get_error_depth(store_ctx);312if (depth < 0)313return 0;314/* If there's an error at this level that we're not ignoring, fail. */315err = X509_STORE_CTX_get_error(store_ctx);316if (err != X509_V_OK) {317bio = BIO_new(BIO_s_mem());318if (bio != NULL) {319X509_NAME_print_ex(bio, X509_get_subject_name(x), 0, 0);320count = BIO_get_mem_data(bio, &cert);321errstr = X509_verify_cert_error_string(err);322TRACE_TLS_CERT_ERROR(context, depth, count, cert, err, errstr);323BIO_free(bio);324}325return 0;326}327/* If we're not looking at the peer, we're done and everything's ok. */328if (depth != 0)329return 1;330/* Check if the name we expect to find is in the certificate. */331expected_name = handle->servername;332if (check_cert_name_or_ip(x, expected_name)) {333TRACE_TLS_SERVER_NAME_MATCH(context, expected_name);334return 1;335} else {336TRACE_TLS_SERVER_NAME_MISMATCH(context, expected_name);337}338/* The name didn't match. */339return 0;340}341342static krb5_error_code343load_anchor_file(X509_STORE *store, const char *path)344{345FILE *fp;346STACK_OF(X509_INFO) *sk = NULL;347X509_INFO *xi;348int i;349350fp = fopen(path, "r");351if (fp == NULL)352return errno;353sk = PEM_X509_INFO_read(fp, NULL, NULL, NULL);354fclose(fp);355if (sk == NULL)356return ENOENT;357for (i = 0; i < sk_X509_INFO_num(sk); i++) {358xi = sk_X509_INFO_value(sk, i);359if (xi->x509 != NULL)360X509_STORE_add_cert(store, xi->x509);361}362sk_X509_INFO_pop_free(sk, X509_INFO_free);363return 0;364}365366static krb5_error_code367load_anchor_dir(X509_STORE *store, const char *path)368{369DIR *d = NULL;370struct dirent *dentry = NULL;371char filename[1024];372krb5_boolean found_any = FALSE;373374d = opendir(path);375if (d == NULL)376return ENOENT;377while ((dentry = readdir(d)) != NULL) {378if (dentry->d_name[0] != '.') {379snprintf(filename, sizeof(filename), "%s/%s",380path, dentry->d_name);381if (load_anchor_file(store, filename) == 0)382found_any = TRUE;383}384}385closedir(d);386return found_any ? 0 : ENOENT;387}388389static krb5_error_code390load_anchor(SSL_CTX *ctx, const char *location)391{392X509_STORE *store;393const char *envloc;394395store = SSL_CTX_get_cert_store(ctx);396if (strncmp(location, "FILE:", 5) == 0) {397return load_anchor_file(store, location + 5);398} else if (strncmp(location, "DIR:", 4) == 0) {399return load_anchor_dir(store, location + 4);400} else if (strncmp(location, "ENV:", 4) == 0) {401envloc = secure_getenv(location + 4);402if (envloc == NULL)403return ENOENT;404return load_anchor(ctx, envloc);405}406return EINVAL;407}408409static krb5_error_code410load_anchors(krb5_context context, char **anchors, SSL_CTX *sctx)411{412unsigned int i;413krb5_error_code ret;414415if (anchors != NULL) {416for (i = 0; anchors[i] != NULL; i++) {417ret = load_anchor(sctx, anchors[i]);418if (ret)419return ret;420}421} else {422/* Use the library defaults. */423if (SSL_CTX_set_default_verify_paths(sctx) != 1)424return ENOENT;425}426427return 0;428}429430static krb5_error_code431setup(krb5_context context, SOCKET fd, const char *servername,432char **anchors, k5_tls_handle *handle_out)433{434int e;435long options = SSL_OP_NO_SSLv2;436SSL_CTX *ctx = NULL;437SSL *ssl = NULL;438k5_tls_handle handle = NULL;439440*handle_out = NULL;441442(void)CALL_INIT_FUNCTION(init_openssl);443if (ex_context_id == -1 || ex_handle_id == -1)444return KRB5_PLUGIN_OP_NOTSUPP;445446/* Do general SSL library setup. */447ctx = SSL_CTX_new(SSLv23_client_method());448if (ctx == NULL)449goto error;450451#ifdef SSL_OP_IGNORE_UNEXPECTED_EOF452/*453* For OpenSSL 3 and later, mark close_notify alerts as optional. We don't454* need to worry about truncation attacks because the protocols this module455* is used with (Kerberos and change-password) receive a single456* length-delimited message from the server. For prior versions of OpenSSL457* we check for SSL_ERROR_SYSCALL when reading instead (this error changes458* to SSL_ERROR_SSL in OpenSSL 3).459*/460options |= SSL_OP_IGNORE_UNEXPECTED_EOF;461#endif462SSL_CTX_set_options(ctx, options);463464SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback);465X509_STORE_set_flags(SSL_CTX_get_cert_store(ctx), 0);466e = load_anchors(context, anchors, ctx);467if (e != 0)468goto error;469470ssl = SSL_new(ctx);471if (ssl == NULL)472goto error;473474if (!SSL_set_fd(ssl, fd))475goto error;476#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME477if (!SSL_set_tlsext_host_name(ssl, servername))478goto error;479#endif480SSL_set_connect_state(ssl);481482/* Create a handle and allow verify_callback to access it. */483handle = malloc(sizeof(*handle));484if (handle == NULL || !SSL_set_ex_data(ssl, ex_handle_id, handle))485goto error;486487handle->ssl = ssl;488handle->servername = strdup(servername);489if (handle->servername == NULL)490goto error;491*handle_out = handle;492SSL_CTX_free(ctx);493return 0;494495error:496flush_errors(context);497free(handle);498SSL_free(ssl);499SSL_CTX_free(ctx);500return KRB5_PLUGIN_OP_NOTSUPP;501}502503static k5_tls_status504write_tls(krb5_context context, k5_tls_handle handle, const void *data,505size_t len)506{507int nwritten, e;508509/* Try to transmit our request; allow verify_callback to access context. */510if (!SSL_set_ex_data(handle->ssl, ex_context_id, context))511return ERROR_TLS;512nwritten = SSL_write(handle->ssl, data, len);513(void)SSL_set_ex_data(handle->ssl, ex_context_id, NULL);514if (nwritten > 0)515return DONE;516517e = SSL_get_error(handle->ssl, nwritten);518if (e == SSL_ERROR_WANT_READ)519return WANT_READ;520else if (e == SSL_ERROR_WANT_WRITE)521return WANT_WRITE;522flush_errors(context);523return ERROR_TLS;524}525526static k5_tls_status527read_tls(krb5_context context, k5_tls_handle handle, void *data,528size_t data_size, size_t *len_out)529{530ssize_t nread;531int e;532533*len_out = 0;534535/* Try to read response data; allow verify_callback to access context. */536if (!SSL_set_ex_data(handle->ssl, ex_context_id, context))537return ERROR_TLS;538nread = SSL_read(handle->ssl, data, data_size);539(void)SSL_set_ex_data(handle->ssl, ex_context_id, NULL);540if (nread > 0) {541*len_out = nread;542return DATA_READ;543}544545e = SSL_get_error(handle->ssl, nread);546if (e == SSL_ERROR_WANT_READ)547return WANT_READ;548else if (e == SSL_ERROR_WANT_WRITE)549return WANT_WRITE;550551if (e == SSL_ERROR_ZERO_RETURN || (e == SSL_ERROR_SYSCALL && nread == 0))552return DONE;553554flush_errors(context);555return ERROR_TLS;556}557558static void559free_handle(krb5_context context, k5_tls_handle handle)560{561SSL_free(handle->ssl);562free(handle->servername);563free(handle);564}565566krb5_error_code567tls_k5tls_initvt(krb5_context context, int maj_ver, int min_ver,568krb5_plugin_vtable vtable);569570krb5_error_code571tls_k5tls_initvt(krb5_context context, int maj_ver, int min_ver,572krb5_plugin_vtable vtable)573{574k5_tls_vtable vt;575576vt = (k5_tls_vtable)vtable;577vt->setup = setup;578vt->write = write_tls;579vt->read = read_tls;580vt->free_handle = free_handle;581return 0;582}583584#endif /* TLS_IMPL_OPENSSL */585586587