diff options
Diffstat (limited to 'src/lib-ssl-iostream/iostream-openssl-common.c')
-rw-r--r-- | src/lib-ssl-iostream/iostream-openssl-common.c | 343 |
1 files changed, 343 insertions, 0 deletions
diff --git a/src/lib-ssl-iostream/iostream-openssl-common.c b/src/lib-ssl-iostream/iostream-openssl-common.c new file mode 100644 index 0000000..04dc5ea --- /dev/null +++ b/src/lib-ssl-iostream/iostream-openssl-common.c @@ -0,0 +1,343 @@ +/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "net.h" +#include "str.h" +#include "iostream-openssl.h" + +#include <openssl/x509v3.h> +#include <openssl/err.h> +#include <arpa/inet.h> + +/* openssl_min_protocol_to_options() scans this array for name and returns + version and opt. opt is used with SSL_set_options() and version is used with + SSL_set_min_proto_version(). Using either method should enable the same + SSL protocol versions. */ +static const struct { + const char *name; + int version; + long opt; +} protocol_versions[] = { +#ifdef TLS_ANY_VERSION + { "ANY", TLS_ANY_VERSION, 0 }, +#else + { "ANY", SSL3_VERSION, 0 }, +#endif + { SSL_TXT_SSLV3, SSL3_VERSION, 0 }, + { SSL_TXT_TLSV1, TLS1_VERSION, SSL_OP_NO_SSLv3 }, + { SSL_TXT_TLSV1_1, TLS1_1_VERSION, SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 }, + { SSL_TXT_TLSV1_2, TLS1_2_VERSION, + SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 }, +#if defined(TLS1_3_VERSION) + { "TLSv1.3", TLS1_3_VERSION, + SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | + SSL_OP_NO_TLSv1_2 }, +#endif + /* Use latest protocol version. If this is used on some + ancient system which does not support ssl_min_protocol, + ensure only TLSv1.2 is supported. */ +#ifdef TLS_MAX_VERSION + { "LATEST", TLS_MAX_VERSION, +#else + { "LATEST", 0, +#endif + SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 }, +}; +int openssl_min_protocol_to_options(const char *min_protocol, long *opt_r, + int *version_r) +{ + unsigned i = 0; + for (; i < N_ELEMENTS(protocol_versions); i++) { + if (strcasecmp(protocol_versions[i].name, min_protocol) == 0) + break; + } + if (i >= N_ELEMENTS(protocol_versions)) + return -1; + + if (opt_r != NULL) + *opt_r = protocol_versions[i].opt; + if (version_r != NULL) + *version_r = protocol_versions[i].version; + return 0; +} + +#if !defined(HAVE_X509_CHECK_HOST) || !defined(HAVE_X509_CHECK_IP_ASC) +static const char *asn1_string_to_c(ASN1_STRING *asn_str) +{ + const char *cstr; + unsigned int len; + + len = ASN1_STRING_length(asn_str); + cstr = t_strndup(ASN1_STRING_get0_data(asn_str), len); + if (strlen(cstr) != len) { + /* NULs in the name - could be some MITM attack. + never allow. */ + return ""; + } + return cstr; +} + +static const char *get_general_dns_name(const GENERAL_NAME *name) +{ + if (ASN1_STRING_type(name->d.ia5) != V_ASN1_IA5STRING) + return ""; + + return asn1_string_to_c(name->d.ia5); +} + +static int get_general_ip_addr(const GENERAL_NAME *name, struct ip_addr *ip_r) +{ + if (ASN1_STRING_type(name->d.ip) != V_ASN1_OCTET_STRING) + return 0; + const unsigned char *data = ASN1_STRING_get0_data(name->d.ip); + + if (name->d.ip->length == sizeof(ip_r->u.ip4.s_addr)) { + ip_r->family = AF_INET; + memcpy(&ip_r->u.ip4.s_addr, data, sizeof(ip_r->u.ip4.s_addr)); + } else if (name->d.ip->length == sizeof(ip_r->u.ip6.s6_addr)) { + ip_r->family = AF_INET6; + memcpy(ip_r->u.ip6.s6_addr, data, sizeof(ip_r->u.ip6.s6_addr)); + } else + return -1; + return 0; +} + +static const char *get_cname(X509 *cert) +{ + X509_NAME *name; + X509_NAME_ENTRY *entry; + ASN1_STRING *str; + int cn_idx; + + name = X509_get_subject_name(cert); + if (name == NULL) + return ""; + cn_idx = X509_NAME_get_index_by_NID(name, NID_commonName, -1); + if (cn_idx == -1) + return ""; + entry = X509_NAME_get_entry(name, cn_idx); + i_assert(entry != NULL); + str = X509_NAME_ENTRY_get_data(entry); + i_assert(str != NULL); + return asn1_string_to_c(str); +} + +static bool openssl_hostname_equals(const char *ssl_name, const char *host) +{ + const char *p; + + if (strcasecmp(ssl_name, host) == 0) + return TRUE; + + /* check for *.example.com wildcard */ + if (ssl_name[0] != '*' || ssl_name[1] != '.') + return FALSE; + p = strchr(host, '.'); + return p != NULL && strcasecmp(ssl_name+2, p+1) == 0; +} +#endif + +bool openssl_cert_match_name(SSL *ssl, const char *verify_name, + const char **reason_r) +{ + X509 *cert; + bool ret; + + *reason_r = NULL; + + cert = SSL_get_peer_certificate(ssl); + i_assert(cert != NULL); + +#if defined(HAVE_X509_CHECK_HOST) && defined(HAVE_X509_CHECK_IP_ASC) + char *peername; + int check_res; + + /* First check DNS name agains CommonName or SubjectAltNames. + If failed, check IP addresses. */ + if ((check_res = X509_check_host(cert, verify_name, strlen(verify_name), + X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS, + &peername)) == 1) { + *reason_r = t_strdup_printf("Matched to %s", peername); + free(peername); + ret = TRUE; + } else if (check_res == 0 && + (check_res = X509_check_ip_asc(cert, verify_name, 0)) == 1) { + *reason_r = t_strdup_printf("Matched to IP address %s", verify_name); + ret = TRUE; + } else if (check_res == 0) { + *reason_r = "did not match to any IP or DNS fields"; + ret = FALSE; + } else { + *reason_r = "Malformed input"; + ret = FALSE; + } +#else + STACK_OF(GENERAL_NAME) *gnames; + const GENERAL_NAME *gn; + struct ip_addr ip; + const char *dnsname; + bool dns_names = FALSE; + unsigned int i, count; + + /* verify against SubjectAltNames */ + gnames = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL); + count = gnames == NULL ? 0 : sk_GENERAL_NAME_num(gnames); + + i_zero(&ip); + /* try to convert verify_name to IP */ + if (inet_pton(AF_INET6, verify_name, &ip.u.ip6) == 1) + ip.family = AF_INET6; + else if (inet_pton(AF_INET, verify_name, &ip.u.ip4) == 1) + ip.family = AF_INET; + else + i_zero(&ip); + + for (i = 0; i < count; i++) { + gn = sk_GENERAL_NAME_value(gnames, i); + + if (gn->type == GEN_DNS) { + dns_names = TRUE; + dnsname = get_general_dns_name(gn); + if (openssl_hostname_equals(dnsname, verify_name)) { + *reason_r = t_strdup_printf( + "Matches DNS name in SubjectAltNames: %s", dnsname); + break; + } + } else if (gn->type == GEN_IPADD) { + struct ip_addr ip_2; + i_zero(&ip_2); + dns_names = TRUE; + if (get_general_ip_addr(gn, &ip_2) == 0 && + net_ip_compare(&ip, &ip_2)) { + *reason_r = t_strdup_printf( + "Matches IP in SubjectAltNames: %s", net_ip2addr(&ip_2)); + break; + } + } + } + sk_GENERAL_NAME_pop_free(gnames, GENERAL_NAME_free); + + /* verify against CommonName only when there wasn't any DNS + SubjectAltNames */ + if (dns_names) { + i_assert(*reason_r != NULL || i == count); + if (i == count) { + *reason_r = t_strdup_printf( + "No match to %u SubjectAltNames", + count); + ret = FALSE; + } else { + ret = TRUE; + } + } else { + const char *cname = get_cname(cert); + + if (openssl_hostname_equals(cname, verify_name)) { + ret = TRUE; + *reason_r = t_strdup_printf( + "Matches to CommonName: %s", cname); + } else { + *reason_r = t_strdup_printf( + "No match to CommonName=%s or %u SubjectAltNames", + cname, count); + ret = FALSE; + } + } +#endif + X509_free(cert); + return ret; +} + +static const char *ssl_err2str(unsigned long err, const char *data, int flags) +{ + const char *ret; + char *buf; + size_t err_size = 256; + + buf = t_malloc0(err_size); + ERR_error_string_n(err, buf, err_size-1); + ret = buf; + + if ((flags & ERR_TXT_STRING) != 0) + ret = t_strdup_printf("%s: %s", buf, data); + return ret; +} + +const char *openssl_iostream_error(void) +{ + string_t *errstr = NULL; + unsigned long err; + const char *data, *final_error; + int flags; + + while ((err = ERR_get_error_line_data(NULL, NULL, &data, &flags)) != 0) { + if (ERR_GET_REASON(err) == ERR_R_MALLOC_FAILURE) + i_fatal_status(FATAL_OUTOFMEM, "OpenSSL malloc() failed"); + if (ERR_peek_error() == 0) + break; + if (errstr == NULL) + errstr = t_str_new(128); + else + str_append(errstr, ", "); + str_append(errstr, ssl_err2str(err, data, flags)); + } + if (err == 0) { + if (errno != 0) + final_error = strerror(errno); + else + final_error = "Unknown error"; + } else { + final_error = ssl_err2str(err, data, flags); + } + if (errstr == NULL) + return final_error; + else { + str_printfa(errstr, ", %s", final_error); + return str_c(errstr); + } +} + +const char *openssl_iostream_key_load_error(void) +{ + unsigned long err = ERR_peek_error(); + + if (ERR_GET_LIB(err) == ERR_LIB_X509 && + ERR_GET_REASON(err) == X509_R_KEY_VALUES_MISMATCH) + return "Key is for a different cert than ssl_cert"; + else + return openssl_iostream_error(); +} + +static bool is_pem_key(const char *cert) +{ + return strstr(cert, "PRIVATE KEY---") != NULL; +} + +const char * +openssl_iostream_use_certificate_error(const char *cert, const char *set_name) +{ + unsigned long err; + + if (cert[0] == '\0') + return "The certificate is empty"; + + err = ERR_peek_error(); + if (ERR_GET_LIB(err) != ERR_LIB_PEM || + ERR_GET_REASON(err) != PEM_R_NO_START_LINE) + return openssl_iostream_error(); + else if (is_pem_key(cert)) { + return "The file contains a private key " + "(you've mixed ssl_cert and ssl_key settings)"; + } else if (set_name != NULL && strchr(cert, '\n') == NULL) { + return t_strdup_printf("There is no valid PEM certificate. " + "(You probably forgot '<' from %s=<%s)", set_name, cert); + } else { + return "There is no valid PEM certificate."; + } +} + +void openssl_iostream_clear_errors(void) +{ + while (ERR_get_error() != 0) + ; +} |