summaryrefslogtreecommitdiffstats
path: root/src/lib-ssl-iostream/iostream-openssl-common.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib-ssl-iostream/iostream-openssl-common.c')
-rw-r--r--src/lib-ssl-iostream/iostream-openssl-common.c343
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)
+ ;
+}