From 545e67eb42a0076198da3b97594aedbc578f8ada Mon Sep 17 00:00:00 2001 From: Alan Antonuk Date: Sun, 8 Nov 2015 23:17:02 -0800 Subject: Lib: Add robust OpenSSL hostname validation. Use vetted code from https://wiki.openssl.org/index.php/Hostname_validation and https://github.com/iSECPartners/ssl-conservatory to do hostname validation of SSL certs when using OpenSSL. --- Makefile.am | 4 +- librabbitmq/CMakeLists.txt | 2 + librabbitmq/amqp_openssl_hostname_validation.c | 167 +++++++++++++++++++++++++ librabbitmq/amqp_openssl_hostname_validation.h | 59 +++++++++ 4 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 librabbitmq/amqp_openssl_hostname_validation.c create mode 100644 librabbitmq/amqp_openssl_hostname_validation.h diff --git a/Makefile.am b/Makefile.am index bd86733..3fa290c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -44,7 +44,9 @@ if SSL_OPENSSL librabbitmq_librabbitmq_la_SOURCES += \ librabbitmq/amqp_hostcheck.c \ librabbitmq/amqp_hostcheck.h \ - librabbitmq/amqp_openssl.c + librabbitmq/amqp_openssl.c \ + librabbitmq/amqp_openssl_hostname_validation.c \ + librabbitmq/amqp_openssl_hostname_validation.h if OS_APPLE librabbitmq_librabbitmq_la_CFLAGS += -Wno-deprecated-declarations endif diff --git a/librabbitmq/CMakeLists.txt b/librabbitmq/CMakeLists.txt index 103742a..b5170d7 100644 --- a/librabbitmq/CMakeLists.txt +++ b/librabbitmq/CMakeLists.txt @@ -82,6 +82,8 @@ if (ENABLE_SSL_SUPPORT) set(AMQP_SSL_SRCS ${AMQP_SSL_SOCKET_H_PATH} amqp_openssl.c + amqp_openssl_hostname_validation.c + amqp_openssl_hostname_validation.h amqp_hostcheck.c amqp_hostcheck.h ) diff --git a/librabbitmq/amqp_openssl_hostname_validation.c b/librabbitmq/amqp_openssl_hostname_validation.c new file mode 100644 index 0000000..5fad5f7 --- /dev/null +++ b/librabbitmq/amqp_openssl_hostname_validation.c @@ -0,0 +1,167 @@ +/* vim:set ft=c ts=2 sw=2 sts=2 et cindent: */ +/* + * Copyright (C) 2012, iSEC Partners. + * Copyright (C) 2015 Alan Antonuk. + * + * All rights reserved. + * + * 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", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * Except as contained in this notice, the name of a copyright holder shall + * not be used in advertising or otherwise to promote the sale, use or other + * dealings in this Software without prior written authorization of the + * copyright holder. + */ + + +/* Originally from: + * https://github.com/iSECPartners/ssl-conservatory + * https://wiki.openssl.org/index.php/Hostname_validation + */ + +#include +#include + +#include "amqp_openssl_hostname_validation.h" +#include "amqp_hostcheck.h" + +#define HOSTNAME_MAX_SIZE 255 + +/** +* Tries to find a match for hostname in the certificate's Common Name field. +* +* Returns AMQP_HVR_MATCH_FOUND if a match was found. +* Returns AMQP_HVR_MATCH_NOT_FOUND if no matches were found. +* Returns AMQP_HVR_MALFORMED_CERTIFICATE if the Common Name had a NUL character embedded in it. +* Returns AMQP_HVR_ERROR if the Common Name could not be extracted. +*/ +static amqp_hostname_validation_result amqp_matches_common_name( + const char *hostname, const X509 *server_cert) { + int common_name_loc = -1; + X509_NAME_ENTRY *common_name_entry = NULL; + ASN1_STRING *common_name_asn1 = NULL; + char *common_name_str = NULL; + + // Find the position of the CN field in the Subject field of the certificate + common_name_loc = X509_NAME_get_index_by_NID( + X509_get_subject_name((X509 *)server_cert), NID_commonName, -1); + if (common_name_loc < 0) { + return AMQP_HVR_ERROR; + } + + // Extract the CN field + common_name_entry = X509_NAME_get_entry( + X509_get_subject_name((X509 *)server_cert), common_name_loc); + if (common_name_entry == NULL) { + return AMQP_HVR_ERROR; + } + + // Convert the CN field to a C string + common_name_asn1 = X509_NAME_ENTRY_get_data(common_name_entry); + if (common_name_asn1 == NULL) { + return AMQP_HVR_ERROR; + } + common_name_str = (char *)ASN1_STRING_data(common_name_asn1); + + // Make sure there isn't an embedded NUL character in the CN + if ((size_t)ASN1_STRING_length(common_name_asn1) != strlen(common_name_str)) { + return AMQP_HVR_MALFORMED_CERTIFICATE; + } + + // Compare expected hostname with the CN + if (strcasecmp(hostname, common_name_str) == 0) { + return AMQP_HVR_MATCH_FOUND; + } else { + return AMQP_HVR_MATCH_NOT_FOUND; + } +} + +/** +* Tries to find a match for hostname in the certificate's Subject Alternative +* Name extension. +* +* Returns AMQP_HVR_MATCH_FOUND if a match was found. +* Returns AMQP_HVR_MATCH_NOT_FOUND if no matches were found. +* Returns AMQP_HVR_MALFORMED_CERTIFICATE if any of the hostnames had a NUL +* character embedded in it. +* Returns AMQP_HVR_NO_SAN_PRESENT if the SAN extension was not present in the +* certificate. +*/ +static amqp_hostname_validation_result amqp_matches_subject_alternative_name( + const char *hostname, const X509 *server_cert) { + amqp_hostname_validation_result result = AMQP_HVR_MATCH_NOT_FOUND; + int i; + int san_names_nb = -1; + STACK_OF(GENERAL_NAME) *san_names = NULL; + + // Try to extract the names within the SAN extension from the certificate + san_names = + X509_get_ext_d2i((X509 *)server_cert, NID_subject_alt_name, NULL, NULL); + if (san_names == NULL) { + return AMQP_HVR_NO_SAN_PRESENT; + } + san_names_nb = sk_GENERAL_NAME_num(san_names); + + // Check each name within the extension + for (i = 0; i < san_names_nb; i++) { + const GENERAL_NAME *current_name = sk_GENERAL_NAME_value(san_names, i); + + if (current_name->type == GEN_DNS) { + // Current name is a DNS name, let's check it + char *dns_name = (char *)ASN1_STRING_data(current_name->d.dNSName); + + // Make sure there isn't an embedded NUL character in the DNS name + if ((size_t)ASN1_STRING_length(current_name->d.dNSName) != + strlen(dns_name)) { + result = AMQP_HVR_MALFORMED_CERTIFICATE; + break; + } else { // Compare expected hostname with the DNS name + if (amqp_hostcheck(hostname, dns_name) == 0) { + result = AMQP_HVR_MATCH_FOUND; + break; + } + } + } + } + sk_GENERAL_NAME_pop_free(san_names, GENERAL_NAME_free); + + return result; +} + +/** +* Validates the server's identity by looking for the expected hostname in the +* server's certificate. As described in RFC 6125, it first tries to find a match +* in the Subject Alternative Name extension. If the extension is not present in +* the certificate, it checks the Common Name instead. +* +* Returns AMQP_HVR_MATCH_FOUND if a match was found. +* Returns AMQP_HVR_MATCH_NOT_FOUND if no matches were found. +* Returns AMQP_HVR_MALFORMED_CERTIFICATE if any of the hostnames had a NUL +* character embedded in it. +* Returns AMQP_HVR_ERROR if there was an error. +*/ +amqp_hostname_validation_result amqp_ssl_validate_hostname( + const char *hostname, const X509 *server_cert) { + amqp_hostname_validation_result result; + + if ((hostname == NULL) || (server_cert == NULL)) return AMQP_HVR_ERROR; + + // First try the Subject Alternative Names extension + result = amqp_matches_subject_alternative_name(hostname, server_cert); + if (result == AMQP_HVR_NO_SAN_PRESENT) { + // Extension was not found: try the Common Name + result = amqp_matches_common_name(hostname, server_cert); + } + + return result; +} diff --git a/librabbitmq/amqp_openssl_hostname_validation.h b/librabbitmq/amqp_openssl_hostname_validation.h new file mode 100644 index 0000000..bd74ee6 --- /dev/null +++ b/librabbitmq/amqp_openssl_hostname_validation.h @@ -0,0 +1,59 @@ +/* vim:set ft=c ts=2 sw=2 sts=2 et cindent: */ +#ifndef librabbitmq_amqp_openssl_hostname_validation_h +#define librabbitmq_amqp_openssl_hostname_validation_h + +/* + * Copyright (C) 2012, iSEC Partners. + * Copyright (C) 2015 Alan Antonuk. + * + * All rights reserved. + * + * 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", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE + * USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * Except as contained in this notice, the name of a copyright holder shall + * not be used in advertising or otherwise to promote the sale, use or other + * dealings in this Software without prior written authorization of the + * copyright holder. + */ + +/* Originally from: + * https://github.com/iSECPartners/ssl-conservatory + * https://wiki.openssl.org/index.php/Hostname_validation + */ + +#include + +typedef enum { + AMQP_HVR_MATCH_FOUND, + AMQP_HVR_MATCH_NOT_FOUND, + AMQP_HVR_NO_SAN_PRESENT, + AMQP_HVR_MALFORMED_CERTIFICATE, + AMQP_HVR_ERROR +} amqp_hostname_validation_result; + +/** +* Validates the server's identity by looking for the expected hostname in the +* server's certificate. As described in RFC 6125, it first tries to find a match +* in the Subject Alternative Name extension. If the extension is not present in +* the certificate, it checks the Common Name instead. +* +* Returns AMQP_HVR_MATCH_FOUND if a match was found. +* Returns AMQP_HVR_MATCH_NOT_FOUND if no matches were found. +* Returns AMQP_HVR_MALFORMED_CERTIFICATE if any of the hostnames had a NUL +* character embedded in it. +* Returns AMQP_HVR_ERROR if there was an error. +*/ +amqp_hostname_validation_result amqp_ssl_validate_hostname( + const char *hostname, const X509 *server_cert); + +#endif -- cgit v1.2.1