|
| 1 | +/* vim:set ft=c ts=2 sw=2 sts=2 et cindent: */ |
| 2 | +/* |
| 3 | + * Copyright (C) 2012, iSEC Partners. |
| 4 | + * Copyright (C) 2015 Alan Antonuk. |
| 5 | + * |
| 6 | + * All rights reserved. |
| 7 | + * |
| 8 | + * Permission to use, copy, modify, and distribute this software for any |
| 9 | + * purpose with or without fee is hereby granted, provided that the above |
| 10 | + * copyright notice and this permission notice appear in all copies. |
| 11 | + * |
| 12 | + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 13 | + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 14 | + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. |
| 15 | + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, |
| 16 | + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
| 17 | + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE |
| 18 | + * USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 19 | + * |
| 20 | + * Except as contained in this notice, the name of a copyright holder shall |
| 21 | + * not be used in advertising or otherwise to promote the sale, use or other |
| 22 | + * dealings in this Software without prior written authorization of the |
| 23 | + * copyright holder. |
| 24 | + */ |
| 25 | + |
| 26 | + |
| 27 | +/* Originally from: |
| 28 | + * https://github.com/iSECPartners/ssl-conservatory |
| 29 | + * https://wiki.openssl.org/index.php/Hostname_validation |
| 30 | + */ |
| 31 | + |
| 32 | +#include <openssl/x509v3.h> |
| 33 | +#include <openssl/ssl.h> |
| 34 | + |
| 35 | +#include "amqp_openssl_hostname_validation.h" |
| 36 | +#include "amqp_hostcheck.h" |
| 37 | + |
| 38 | +#define HOSTNAME_MAX_SIZE 255 |
| 39 | + |
| 40 | +/** |
| 41 | +* Tries to find a match for hostname in the certificate's Common Name field. |
| 42 | +* |
| 43 | +* Returns AMQP_HVR_MATCH_FOUND if a match was found. |
| 44 | +* Returns AMQP_HVR_MATCH_NOT_FOUND if no matches were found. |
| 45 | +* Returns AMQP_HVR_MALFORMED_CERTIFICATE if the Common Name had a NUL character embedded in it. |
| 46 | +* Returns AMQP_HVR_ERROR if the Common Name could not be extracted. |
| 47 | +*/ |
| 48 | +static amqp_hostname_validation_result amqp_matches_common_name( |
| 49 | + const char *hostname, const X509 *server_cert) { |
| 50 | + int common_name_loc = -1; |
| 51 | + X509_NAME_ENTRY *common_name_entry = NULL; |
| 52 | + ASN1_STRING *common_name_asn1 = NULL; |
| 53 | + char *common_name_str = NULL; |
| 54 | + |
| 55 | + // Find the position of the CN field in the Subject field of the certificate |
| 56 | + common_name_loc = X509_NAME_get_index_by_NID( |
| 57 | + X509_get_subject_name((X509 *)server_cert), NID_commonName, -1); |
| 58 | + if (common_name_loc < 0) { |
| 59 | + return AMQP_HVR_ERROR; |
| 60 | + } |
| 61 | + |
| 62 | + // Extract the CN field |
| 63 | + common_name_entry = X509_NAME_get_entry( |
| 64 | + X509_get_subject_name((X509 *)server_cert), common_name_loc); |
| 65 | + if (common_name_entry == NULL) { |
| 66 | + return AMQP_HVR_ERROR; |
| 67 | + } |
| 68 | + |
| 69 | + // Convert the CN field to a C string |
| 70 | + common_name_asn1 = X509_NAME_ENTRY_get_data(common_name_entry); |
| 71 | + if (common_name_asn1 == NULL) { |
| 72 | + return AMQP_HVR_ERROR; |
| 73 | + } |
| 74 | + common_name_str = (char *)ASN1_STRING_data(common_name_asn1); |
| 75 | + |
| 76 | + // Make sure there isn't an embedded NUL character in the CN |
| 77 | + if ((size_t)ASN1_STRING_length(common_name_asn1) != strlen(common_name_str)) { |
| 78 | + return AMQP_HVR_MALFORMED_CERTIFICATE; |
| 79 | + } |
| 80 | + |
| 81 | + // Compare expected hostname with the CN |
| 82 | + if (strcasecmp(hostname, common_name_str) == 0) { |
| 83 | + return AMQP_HVR_MATCH_FOUND; |
| 84 | + } else { |
| 85 | + return AMQP_HVR_MATCH_NOT_FOUND; |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +/** |
| 90 | +* Tries to find a match for hostname in the certificate's Subject Alternative |
| 91 | +* Name extension. |
| 92 | +* |
| 93 | +* Returns AMQP_HVR_MATCH_FOUND if a match was found. |
| 94 | +* Returns AMQP_HVR_MATCH_NOT_FOUND if no matches were found. |
| 95 | +* Returns AMQP_HVR_MALFORMED_CERTIFICATE if any of the hostnames had a NUL |
| 96 | +* character embedded in it. |
| 97 | +* Returns AMQP_HVR_NO_SAN_PRESENT if the SAN extension was not present in the |
| 98 | +* certificate. |
| 99 | +*/ |
| 100 | +static amqp_hostname_validation_result amqp_matches_subject_alternative_name( |
| 101 | + const char *hostname, const X509 *server_cert) { |
| 102 | + amqp_hostname_validation_result result = AMQP_HVR_MATCH_NOT_FOUND; |
| 103 | + int i; |
| 104 | + int san_names_nb = -1; |
| 105 | + STACK_OF(GENERAL_NAME) *san_names = NULL; |
| 106 | + |
| 107 | + // Try to extract the names within the SAN extension from the certificate |
| 108 | + san_names = |
| 109 | + X509_get_ext_d2i((X509 *)server_cert, NID_subject_alt_name, NULL, NULL); |
| 110 | + if (san_names == NULL) { |
| 111 | + return AMQP_HVR_NO_SAN_PRESENT; |
| 112 | + } |
| 113 | + san_names_nb = sk_GENERAL_NAME_num(san_names); |
| 114 | + |
| 115 | + // Check each name within the extension |
| 116 | + for (i = 0; i < san_names_nb; i++) { |
| 117 | + const GENERAL_NAME *current_name = sk_GENERAL_NAME_value(san_names, i); |
| 118 | + |
| 119 | + if (current_name->type == GEN_DNS) { |
| 120 | + // Current name is a DNS name, let's check it |
| 121 | + char *dns_name = (char *)ASN1_STRING_data(current_name->d.dNSName); |
| 122 | + |
| 123 | + // Make sure there isn't an embedded NUL character in the DNS name |
| 124 | + if ((size_t)ASN1_STRING_length(current_name->d.dNSName) != |
| 125 | + strlen(dns_name)) { |
| 126 | + result = AMQP_HVR_MALFORMED_CERTIFICATE; |
| 127 | + break; |
| 128 | + } else { // Compare expected hostname with the DNS name |
| 129 | + if (amqp_hostcheck(hostname, dns_name) == 0) { |
| 130 | + result = AMQP_HVR_MATCH_FOUND; |
| 131 | + break; |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + } |
| 136 | + sk_GENERAL_NAME_pop_free(san_names, GENERAL_NAME_free); |
| 137 | + |
| 138 | + return result; |
| 139 | +} |
| 140 | + |
| 141 | +/** |
| 142 | +* Validates the server's identity by looking for the expected hostname in the |
| 143 | +* server's certificate. As described in RFC 6125, it first tries to find a match |
| 144 | +* in the Subject Alternative Name extension. If the extension is not present in |
| 145 | +* the certificate, it checks the Common Name instead. |
| 146 | +* |
| 147 | +* Returns AMQP_HVR_MATCH_FOUND if a match was found. |
| 148 | +* Returns AMQP_HVR_MATCH_NOT_FOUND if no matches were found. |
| 149 | +* Returns AMQP_HVR_MALFORMED_CERTIFICATE if any of the hostnames had a NUL |
| 150 | +* character embedded in it. |
| 151 | +* Returns AMQP_HVR_ERROR if there was an error. |
| 152 | +*/ |
| 153 | +amqp_hostname_validation_result amqp_ssl_validate_hostname( |
| 154 | + const char *hostname, const X509 *server_cert) { |
| 155 | + amqp_hostname_validation_result result; |
| 156 | + |
| 157 | + if ((hostname == NULL) || (server_cert == NULL)) return AMQP_HVR_ERROR; |
| 158 | + |
| 159 | + // First try the Subject Alternative Names extension |
| 160 | + result = amqp_matches_subject_alternative_name(hostname, server_cert); |
| 161 | + if (result == AMQP_HVR_NO_SAN_PRESENT) { |
| 162 | + // Extension was not found: try the Common Name |
| 163 | + result = amqp_matches_common_name(hostname, server_cert); |
| 164 | + } |
| 165 | + |
| 166 | + return result; |
| 167 | +} |
0 commit comments