[v1,3/3] HostAPD: Add 'check_cert_subject' support

Message ID 1538417575-35315-3-git-send-email-jared.bents@rockwellcollins.com
State New
Headers show
Series
  • [v1,1/3] HostAPD: Add option 'check_crl_strict'
Related show

Commit Message

Jared Bents Oct. 1, 2018, 6:12 p.m.
This patch added 'check_cert_subject' support to match the value of
every field against the DN of the subject in the client certificate. If
the values do not match, the certificate verification will fail and will reject
the user.

This option allows hostapd to match every individual field in the right order,
also allow '*' character as a wildcard (e.g OU=Development*).

Note: Hostapd will match string up to 'wildcard' against the DN of the subject
in the client certificate for every individual field.

Signed-off-by: Paresh Chaudhary <paresh.chaudhary@rockwellcollins.com>
Signed-off-by: Jared Bents <jared.bents@rockwellcollins.com>
---
 hostapd/config_file.c    |   8 ++
 hostapd/hostapd.conf     |  27 ++++
 src/ap/ap_config.c       |   1 +
 src/ap/ap_config.h       |   1 +
 src/ap/authsrv.c         |   2 +-
 src/crypto/tls.h         |  23 +++-
 src/crypto/tls_openssl.c | 345 ++++++++++++++++++++++++++++++++++++++++++++++-
 7 files changed, 403 insertions(+), 4 deletions(-)

Patch

diff --git a/hostapd/config_file.c b/hostapd/config_file.c
index f5b8e83..a9bb403 100644
--- a/hostapd/config_file.c
+++ b/hostapd/config_file.c
@@ -2129,6 +2129,14 @@  static int hostapd_config_fill(struct hostapd_config *conf,
 	} else if (os_strcmp(buf, "private_key_passwd") == 0) {
 		os_free(bss->private_key_passwd);
 		bss->private_key_passwd = os_strdup(pos);
+	} else if (os_strcmp(buf, "check_cert_subject") == 0) {
+		os_free(bss->check_cert_subject);
+		bss->check_cert_subject = os_strdup(pos);
+		if (!strlen(bss->check_cert_subject)) {
+			wpa_printf(MSG_ERROR, "Line %d: unknown check_cert_subject '%s'",
+				   line, pos);
+			return 1;
+		}
 	} else if (os_strcmp(buf, "check_crl") == 0) {
 		bss->check_crl = atoi(pos);
 	} else if (os_strcmp(buf, "check_crl_strict") == 0) {
diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf
index c9c6e41..7d69738 100644
--- a/hostapd/hostapd.conf
+++ b/hostapd/hostapd.conf
@@ -811,6 +811,33 @@  eap_server=0
 # 0 = do not reload CRLS (default)
 # crl_reload_interval = 300
 
+# If check_cert_subject is set, the value of every field will be checked
+# against the DN of the subject in the client certificate. If the values do
+# not match, the certificate verification will fail, rejecting the user.
+# This option allows hostapd to match every individual field in the right order
+# against the dn of the subject in the client certificate.
+# [
+# e.g, check_cert_subject=C=US/O=XX/OU=ABC/OU=XYZ/CN=1234, In this case,
+# hostapd will check every individual dn field of the subject in the client
+# certificate, If OU=XYZ comes first in terms of the order in the client certificate
+# (Dn field of client cert= C=US/O=XX/OU=XYZ/OU=ABC/CN=1234) dn field then
+# hostapd will reject client because the order of 'OU' is not matching the specified
+# string in 'check_cert_subject'.
+# ]
+# This option also allows '*' as a wildcard. This option has some limitation. It's
+# only work as per below example.
+# [
+# e.g, check_cert_subject=C=US/O=XX/OU=Production*, For example,
+# we have two clients and DN of the subject in the first client certificate is
+# (C=US/O=XX/OU=Production Unit) and dn of the subject in the second client is
+# (C=US/O=XX/OU=Production Factory). In this case, hostapd will allow both clients
+# because the value of 'OU' field in both clients certificate is matching with 'OU'
+# value in 'check_cert_subject' up to 'wildcard'.
+# ]
+# * (Allow all client e.g check_cert_subject = *)
+# check_cert_subject = string
+
+
 # TLS Session Lifetime in seconds
 # This can be used to allow TLS sessions to be cached and resumed with an
 # abbreviated handshake when using EAP-TLS/TTLS/PEAP.
diff --git a/src/ap/ap_config.c b/src/ap/ap_config.c
index 2e3797b..4907811 100644
--- a/src/ap/ap_config.c
+++ b/src/ap/ap_config.c
@@ -466,6 +466,7 @@  void hostapd_config_free_bss(struct hostapd_bss_config *conf)
 	os_free(conf->server_cert);
 	os_free(conf->private_key);
 	os_free(conf->private_key_passwd);
+	os_free(conf->check_cert_subject);
 	os_free(conf->ocsp_stapling_response);
 	os_free(conf->ocsp_stapling_response_multi);
 	os_free(conf->dh_file);
diff --git a/src/ap/ap_config.h b/src/ap/ap_config.h
index fef966c..b168873 100644
--- a/src/ap/ap_config.h
+++ b/src/ap/ap_config.h
@@ -351,6 +351,7 @@  struct hostapd_bss_config {
 	char *server_cert;
 	char *private_key;
 	char *private_key_passwd;
+	char *check_cert_subject;
 	int check_crl;
 	int check_crl_strict;
 	unsigned int crl_reload_interval;
diff --git a/src/ap/authsrv.c b/src/ap/authsrv.c
index 2add858..f2b7a45 100644
--- a/src/ap/authsrv.c
+++ b/src/ap/authsrv.c
@@ -192,7 +192,7 @@  int authsrv_init(struct hostapd_data *hapd)
 
 		if (tls_global_set_verify(hapd->ssl_ctx,
 					  hapd->conf->check_crl,
-					  hapd->conf->check_crl_strict)) {
+					  hapd->conf->check_crl_strict, hapd->conf->check_cert_subject)) {
 			wpa_printf(MSG_ERROR, "Failed to enable check_crl");
 			authsrv_deinit(hapd);
 			return -1;
diff --git a/src/crypto/tls.h b/src/crypto/tls.h
index 85b6a5e..8915774 100644
--- a/src/crypto/tls.h
+++ b/src/crypto/tls.h
@@ -26,6 +26,25 @@  enum tls_event {
 	TLS_ALERT
 };
 
+struct tls_dn_field_order_cnt {
+	uint8_t cn;
+	uint8_t c;
+	uint8_t l;
+	uint8_t st;
+	uint8_t o;
+	uint8_t ou;
+	uint8_t email;
+};
+
+enum digest_alg{
+	DIGEST_HASH_ALG_MD5,
+	DIGEST_HASH_ALG_SHA,
+	DIGEST_HASH_ALG_SHA1,
+	DIGEST_HASH_ALG_SHA224,
+	DIGEST_HASH_ALG_SHA256,
+	DIGEST_HASH_ALG_SHA384,
+	DIGEST_HASH_ALG_SHA512
+};
 /*
  * Note: These are used as identifier with external programs and as such, the
  * values must not be changed.
@@ -42,6 +61,7 @@  enum tls_fail_reason {
 	TLS_FAIL_SERVER_CHAIN_PROBE = 8,
 	TLS_FAIL_DOMAIN_SUFFIX_MISMATCH = 9,
 	TLS_FAIL_DOMAIN_MISMATCH = 10,
+	TLS_FAIL_DN_MISMATCH = 11
 };
 
 
@@ -315,9 +335,10 @@  int __must_check tls_global_set_params(
  * @check_crl: 0 = do not verify CRLs, 1 = verify CRL for the user certificate,
  * 2 = verify CRL for all certificates
  * @strict: 0 = allow time errors, 1 = do not allow time errors
+ * @check_cert_subject : Hostapd configuration 'check_cert_subject' string pointer
  * Returns: 0 on success, -1 on failure
  */
-int __must_check tls_global_set_verify(void *tls_ctx, int check_crl, int strict);
+int __must_check tls_global_set_verify(void *tls_ctx, int check_crl, int strict, char *check_cert_subject);
 
 /**
  * tls_connection_set_verify - Set certificate verification options
diff --git a/src/crypto/tls_openssl.c b/src/crypto/tls_openssl.c
index 41fbf1b..46e6def 100644
--- a/src/crypto/tls_openssl.c
+++ b/src/crypto/tls_openssl.c
@@ -182,6 +182,7 @@  static int tls_add_ca_from_keystore_encoded(X509_STORE *ctx,
 
 static int tls_openssl_ref_count = 0;
 static int tls_ex_idx_session = -1;
+struct tls_dn_field_order_cnt dn_cnt = {0};
 
 struct tls_context {
 	void (*event_cb)(void *ctx, enum tls_event ev,
@@ -192,6 +193,7 @@  struct tls_context {
 	int check_crl_strict;
 	int check_crl;
 	const char *ca_cert;
+	char *check_cert_subject;
 };
 
 static struct tls_context *tls_global = NULL;
@@ -203,6 +205,7 @@  struct tls_data {
 	unsigned int crl_reload_interval;
 	unsigned int crl_last_reload;
 	X509_STORE   *old_x509_store;
+	char *client_cert_subject;
 	pthread_mutex_t	mutex;
 };
 
@@ -1082,6 +1085,10 @@  void tls_deinit(void *ssl_ctx)
 	}
 
 	pthread_mutex_destroy(&data->mutex);
+	if(data->client_cert_subject) {
+		os_free(data->client_cert_subject);
+		data->client_cert_subject = NULL;
+	}
 	os_free(data);
 }
 
@@ -1359,6 +1366,11 @@  struct tls_connection * tls_connection_init(void *ssl_ctx)
 	time_t now;
 	struct tls_context *context = SSL_CTX_get_app_data(ssl);
 
+	if (data->client_cert_subject)
+		tls_global->check_cert_subject = data->client_cert_subject;
+	else
+		tls_global->check_cert_subject = NULL;
+
 	/* Get current time */
 	now = time(NULL);
 
@@ -1580,6 +1592,319 @@  static int domain_suffix_match(const u8 *val, size_t len, const char *match,
 }
 #endif /* CONFIG_NATIVE_WINDOWS */
 
+/**
+ * client_cert_fingerprint - print fingerprint of certificate
+ * @cert: Certificate
+ * @alg:  hash algorithm
+ * Returns: Return 1 on success and 0 on Failure
+*/
+static int client_cert_fingerprint(X509* cert, enum digest_alg alg)
+{
+
+	/* Maximum size of fingerprint with sha512 is 191 bytes,
+	so it's enough to hold fingerprint value for supported algorithm */
+	uint8_t fingerprint[192];
+	char fingerprint_string[192];
+	int ret = 0, i;
+	int pos = 0;
+	uint32_t len = sizeof(fingerprint);
+	uint32_t buflen = sizeof(fingerprint_string);
+
+	/* Init out buffers to zero */
+	os_memset(fingerprint, 0x00, sizeof(fingerprint));
+	os_memset(fingerprint_string, 0x00, sizeof(fingerprint_string));
+
+	switch (alg)
+	{
+		case DIGEST_HASH_ALG_MD5:
+			/* Get the digest */
+			ret = X509_digest(cert, EVP_md5(), fingerprint, &len);
+			break;
+		case DIGEST_HASH_ALG_SHA:
+			/* Get the digest */
+			ret = X509_digest(cert, EVP_sha(), fingerprint, &len);
+			break;
+		case DIGEST_HASH_ALG_SHA1:
+			/* Get the digest */
+			ret = X509_digest(cert, EVP_sha1(), fingerprint, &len);
+			break;
+		case DIGEST_HASH_ALG_SHA224:
+			/* Get the digest */
+			ret = X509_digest(cert, EVP_sha224(), fingerprint, &len);
+			break;
+		case DIGEST_HASH_ALG_SHA256:
+			/* Get the digest */
+			ret = X509_digest(cert, EVP_sha256(), fingerprint, &len);
+			break;
+		case DIGEST_HASH_ALG_SHA384:
+			/* Get the digest */
+			ret = X509_digest(cert, EVP_sha384(), fingerprint, &len);
+			break;
+		case DIGEST_HASH_ALG_SHA512:
+			/* Get the digest */
+			ret = X509_digest(cert, EVP_sha512(), fingerprint, &len);
+			break;
+		default:
+			wpa_printf(MSG_ERROR, "Unknown digest algorithm");
+			break;
+	}
+
+	if (ret != 1) {
+		wpa_printf(MSG_ERROR, "Cannot get digest from certificate");
+		return ret;
+	}
+
+
+	for(i = 0; i < len; ++i) {
+		if (i > 0) {
+			pos += snprintf(fingerprint_string + pos, buflen - pos, ":");
+		}
+		pos += snprintf(fingerprint_string + pos, buflen - pos, "%02X", fingerprint[i]);
+	}
+
+	wpa_printf(MSG_INFO,"Fingerprint: %s\n", fingerprint_string);
+
+	return ret;
+}
+
+/**
+ * match_dn_field - Match configuration DN field value with Certificate DN field value
+ * @cert: Certificate
+ * @nid:  NID of DN field
+ * @value DN field value which is passed from configuration file
+ *        e.g (If configuration have C=US and this argument will point to US)
+ * Returns: Return 1 on success and 0 on Failure
+ */
+static int match_dn_field(X509 *cert, int nid, char *value)
+{
+	int i = -1, ret = 1, len, config_dn_field_index = 0 ;
+	int match_index = 0;
+	X509_NAME *name;
+
+	len = os_strlen(value);
+	name = X509_get_subject_name(cert);
+
+	/* Assign incremented cnt for every field of DN to check DN field in
+	right order */
+	switch (nid)
+	{
+		case NID_commonName:
+			config_dn_field_index = dn_cnt.cn;
+			break;
+		case NID_countryName:
+			config_dn_field_index = dn_cnt.c;
+			break;
+		case NID_localityName:
+			config_dn_field_index = dn_cnt.l;
+			break;
+		case NID_stateOrProvinceName:
+			config_dn_field_index = dn_cnt.st;
+			break;
+		case NID_organizationName:
+			config_dn_field_index = dn_cnt.o;
+			break;
+		case NID_organizationalUnitName:
+			config_dn_field_index = dn_cnt.ou;
+			break;
+		case NID_pkcs9_emailAddress:
+			config_dn_field_index = dn_cnt.email;
+			break;
+		default:
+			wpa_printf(MSG_ERROR,
+				"TLS: Unknown NID '%d' in 'check_cert_subject' "
+				"option of hostapd configuration", nid);
+			return 0;
+	}
+
+	/* Fetch value based on NID */
+	for (;;) {
+		X509_NAME_ENTRY *e;
+		ASN1_STRING *cn;
+		i = X509_NAME_get_index_by_NID(name, nid, i);
+		if (i == -1) {
+			ret = 0;
+			break;
+		}
+		e = X509_NAME_get_entry(name, i);
+		if (e == NULL)
+			continue;
+
+		cn = X509_NAME_ENTRY_get_data(e);
+		if (cn == NULL)
+			continue;
+
+		match_index ++;
+
+		/* check for more than one dn field with same name */
+		if(match_index != config_dn_field_index)
+			continue;
+
+		/* Check wild card at the right end side*/
+		/* e.g. if OU=develop* mentioned in configuration file then hostapd will allow if the
+		'OU' of the subject in the client certificate start with 'develop*' */
+		/* Same applicable for other field of DN*/
+		if( '*' == *(value + len - 1)) {
+		/* Compare actual certificate dn field value with configuration file dn field value upto specified length */
+			if (!os_strncasecmp(cn->data, value, len - 1)) {
+				ret = 1;
+				break;
+			} else {
+				wpa_printf(MSG_ERROR,
+					"TLS: Failed to match '%s' with "
+					"Certificate Distinguished Name '%s'",
+					value, ASN1_STRING_data(cn));
+				wpa_printf(MSG_INFO, "TLS: Please check hostapd configuration");
+				ret = 0;
+				break;
+			}
+		} else {
+		/* Compare actual certificate dn field value with configuration file dn field value */
+			if (!os_strcmp(cn->data, value)) {
+				ret = 1;
+				break;
+			} else {
+				wpa_printf(MSG_ERROR,
+					"TLS: Failed to match '%s' with "
+					"Certificate Distinguished Name '%s'",
+					value, ASN1_STRING_data(cn));
+				wpa_printf(MSG_INFO, "TLS: Please check hostapd configuration");
+				ret = 0;
+				break;
+			}
+		}
+	}
+
+	return ret;
+}
+
+/**
+ * get_value_from_field - Get value from DN field
+ * @cert: Certificate
+ * @field_str:  DN field string which is passed from configuration file (e.g C=US)
+ * Returns: Return 1 on success and 0 on Failure
+ */
+static int get_value_from_field(X509 *cert, char *field_str)
+{
+	int nid = -1, ret = 1;
+	char *temp, *dbg_dn;
+
+	temp = strtok(field_str,"=");
+
+	/* Compare all hostapd configuration DN field and assign nid based on that to
+	fetch correct value from certificate subject*/
+	if(strcmp(temp,"CN") == 0) {
+		nid = NID_commonName;
+		dn_cnt.cn++;
+	} else if(strcmp(temp,"C") == 0) {
+		nid = NID_countryName;
+		dn_cnt.c++;
+	} else if (strcmp(temp,"L") == 0) {
+		nid = NID_localityName;
+		dn_cnt.l++;
+	} else if (strcmp(temp,"ST") == 0) {
+		nid = NID_stateOrProvinceName;
+		dn_cnt.st++;
+	} else if (strcmp(temp,"O") == 0) {
+		nid = NID_organizationName;
+		dn_cnt.o++;
+	} else if (strcmp(temp,"OU") == 0) {
+		nid = NID_organizationalUnitName;
+		dn_cnt.ou++;
+	} else if (strcmp(temp,"emailAddress") == 0) {
+		nid = NID_pkcs9_emailAddress;
+		dn_cnt.email++;
+	} else if (strcmp(temp,"*") == 0) {
+		ret = 1;
+	} else {
+		wpa_printf(MSG_ERROR,
+			"TLS: Unknown field '%s' in 'check_cert_subject' "
+			"option of hostapd configuration", temp);
+		ret = 0;
+	}
+
+	dbg_dn = temp;
+	/* Check for correct NID */
+	if ( (nid >= NID_commonName && nid <= NID_organizationalUnitName) ||
+		nid == NID_pkcs9_emailAddress) {
+		temp = strtok (NULL,"=");
+		if (temp != NULL) {
+			ret = match_dn_field(cert, nid, temp);
+		}
+		else {
+			ret = 0;
+			wpa_printf(MSG_ERROR,
+				"TLS:  Distinguished Name field '%s' value "
+				"is not defined in 'check_cert_subject'. "
+				"Please Check hostapd configuration file", dbg_dn);
+		}
+	}
+
+	return ret;
+}
+
+/**
+ * tls_match_dn_field - Match Certificate individual subject field with check_cert_subject
+ * @cert:   Certificate
+ * @match:  check_cert_subject string
+ * Returns: Return 1 on success and 0 on Failure
+*/
+static int tls_match_dn_field(X509 *cert, const char *match)
+{
+	int match_loop = 0, field_loop = 0, length, last_index = 0, len, ret = 1;
+	char *field = NULL;
+	char buf[2048] = {0}; /* NOTE: Print maximum 2048 bytes of subject and issuer */
+	enum digest_alg alg = DIGEST_HASH_ALG_SHA256; /* Default calculate certificate fingerprint with SHA256 algorithm */
+
+	os_memset(&dn_cnt, 0, sizeof(dn_cnt));
+	len = os_strlen(match);
+
+	/* Maximum length of each DN field is 128 character */
+	field = os_malloc(256);
+	if (field == NULL) {
+		wpa_printf(MSG_ERROR, "TLS: Failed to allocate memory");
+		return 0;
+	}
+	os_memset(field,0,256);
+
+	while ( match[match_loop] != '\0') {
+		/* Stop at '/' character */
+		if (match[match_loop] == '/') {
+			field[field_loop] = '\0';
+			if (strlen(field) > 0) {
+				if(!get_value_from_field(cert, field)) {
+					ret = 0;
+					break;
+				}
+			}
+			os_memset(field,0,256);
+			field_loop = 0;
+			last_index = match_loop;
+			last_index++;
+		} else {
+			field[field_loop++] = match[match_loop];
+		}
+		match_loop++;
+	}
+	if (strlen(field) > 0 && ret != 0) {
+		strncpy(field, match+last_index, len - last_index);
+		if(!get_value_from_field(cert, field))
+			ret = 0;
+	}
+	if(field != NULL)
+		os_free(field);
+
+	X509_NAME_oneline(X509_get_subject_name(cert), buf, sizeof(buf));
+	wpa_printf(MSG_INFO,"TLS: Certificate subject= %s\n", buf);
+	os_memset(buf,0,2048);
+	X509_NAME_oneline(X509_get_issuer_name(cert), buf, sizeof(buf));
+	wpa_printf(MSG_INFO,"TLS: Certificate issuer= %s\n", buf);
+	/* Return 0 only if client_cert_fingerprint function return failure code, otherwise
+	return 'tls_match_dn_field' function return code */
+	if(!client_cert_fingerprint(cert, alg))
+		ret = 0;
+
+	return ret;
+}
 
 static int tls_match_suffix(X509 *cert, const char *match, int full)
 {
@@ -1935,6 +2260,14 @@  static int tls_verify_cb(int preverify_ok, X509_STORE_CTX *x509_ctx)
 		   "err=%d (%s) ca_cert_verify=%d depth=%d buf='%s'",
 		   preverify_ok, err, err_str,
 		   conn->ca_cert_verify, depth, buf);
+	if (tls_global->check_cert_subject) {
+		if(depth == 0 && !tls_match_dn_field(err_cert,tls_global->check_cert_subject)) {
+			preverify_ok = 0;
+			openssl_tls_fail_event(conn, err_cert, err, depth, buf,
+				               "Distinguished Name",
+						TLS_FAIL_DN_MISMATCH);
+		}
+	}
 	if (depth == 0 && match && os_strstr(buf, match) == NULL) {
 		wpa_printf(MSG_WARNING, "TLS: Subject '%s' did not "
 			   "match with '%s'", buf, match);
@@ -2283,14 +2616,22 @@  static int tls_global_ca_cert(struct tls_data *data, const char *ca_cert)
 }
 
 
-int tls_global_set_verify(void *ssl_ctx, int check_crl, int strict)
+int tls_global_set_verify(void *ssl_ctx, int check_crl, int strict, char *check_cert_subject)
 {
 	int flags;
 	SSL *ssl;
 	struct tls_connection *conn;
+	struct tls_data *data = ssl_ctx;
+
+	os_free(data->client_cert_subject);
+	data->client_cert_subject = NULL;
+	if (check_cert_subject) {
+		data->client_cert_subject = os_strdup(check_cert_subject);
+		if (data->client_cert_subject == NULL)
+			return -1;
+	}
 
 	if (check_crl) {
-		struct tls_data *data = ssl_ctx;
 		X509_STORE *cs = SSL_CTX_get_cert_store(data->ssl);
 		if (cs == NULL) {
 			tls_show_errors(MSG_INFO, __func__, "Failed to get "