diff mbox series

[v2,2/2] hostapd: Add 'check_cert_subject' support

Message ID 20190116151502.24590-2-jared.bents@rockwellcollins.com
State Superseded
Headers show
Series [v2,1/2] crl_reload_interval: Add reload crl support | expand

Commit Message

Jared Bents Jan. 16, 2019, 3:15 p.m. UTC
From: Jared Bents <jared.bents@rockwellcollins.com>

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>
--
[v1->v2]: Rebased for currnet hostapd and some upstream feedback on v1
          - Remove unnecessary conditional
          - Due to time constraints, I am not able to address the other
            issues from the feedback

Signed-off-by: Jared Bents <jared.bents@rockwellcollins.com>
---
 hostapd/config_file.c    |   8 +
 hostapd/hostapd.conf     |  26 +++
 src/ap/ap_config.c       |   1 +
 src/ap/ap_config.h       |   1 +
 src/ap/authsrv.c         |   3 +-
 src/crypto/tls.h         |  25 ++-
 src/crypto/tls_openssl.c | 346 ++++++++++++++++++++++++++++++++++++++-
 7 files changed, 406 insertions(+), 4 deletions(-)
diff mbox series

Patch

diff --git a/hostapd/config_file.c b/hostapd/config_file.c
index c69a1a536..af13e73ee 100644
--- a/hostapd/config_file.c
+++ b/hostapd/config_file.c
@@ -2476,6 +2476,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 425ec9a0a..83d2707f9 100644
--- a/hostapd/hostapd.conf
+++ b/hostapd/hostapd.conf
@@ -914,6 +914,32 @@  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 a5655c673..06998268b 100644
--- a/src/ap/ap_config.c
+++ b/src/ap/ap_config.c
@@ -537,6 +537,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 19dd84aad..c9328cbe2 100644
--- a/src/ap/ap_config.h
+++ b/src/ap/ap_config.h
@@ -383,6 +383,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 4f2d418fb..bf1c986c0 100644
--- a/src/ap/authsrv.c
+++ b/src/ap/authsrv.c
@@ -238,7 +238,8 @@  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 0a8d38c76..62a841937 100644
--- a/src/crypto/tls.h
+++ b/src/crypto/tls.h
@@ -25,6 +25,26 @@  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 +62,7 @@  enum tls_fail_reason {
 	TLS_FAIL_DOMAIN_SUFFIX_MISMATCH = 9,
 	TLS_FAIL_DOMAIN_MISMATCH = 10,
 	TLS_FAIL_INSUFFICIENT_KEY_LEN = 11,
+	TLS_FAIL_DN_MISMATCH = 12
 };
 
 
@@ -323,10 +344,12 @@  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 CRL time errors, 1 = do not allow CRL 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 strict,
+				       const 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 77c77b36c..b3cbc952b 100644
--- a/src/crypto/tls_openssl.c
+++ b/src/crypto/tls_openssl.c
@@ -197,6 +197,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,
@@ -204,6 +205,7 @@  struct tls_context {
 	void *cb_ctx;
 	int cert_in_cb;
 	char *ocsp_stapling_response;
+	char *check_cert_subject;
 };
 
 static struct tls_context *tls_global = NULL;
@@ -218,6 +220,7 @@  struct tls_data {
 	unsigned int crl_reload_interval;
 	unsigned int crl_last_reload;
 	X509_STORE   *old_x509_store;
+	char *client_cert_subject;
 };
 
 struct tls_connection {
@@ -1133,6 +1136,10 @@  void tls_deinit(void *ssl_ctx)
 		tls_global = NULL;
 	}
 
+	if(data->client_cert_subject) {
+		os_free(data->client_cert_subject);
+		data->client_cert_subject = NULL;
+	}
 	os_free(data);
 }
 
@@ -1511,6 +1518,8 @@  struct tls_connection * tls_connection_init(void *ssl_ctx)
 	struct os_reltime now;
 	struct tls_context *context = SSL_CTX_get_app_data(ssl);
 
+	tls_global->check_cert_subject = data->client_cert_subject;
+
 	/* Get current time */
 	if (os_get_reltime(&now) < 0)
 		wpa_printf(MSG_ERROR,"Error getting relative time for crl reload");
@@ -1754,6 +1763,323 @@  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)
 {
 #ifdef CONFIG_NATIVE_WINDOWS
@@ -2118,6 +2444,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);
@@ -2464,13 +2798,21 @@  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, const char *check_cert_subject)
 {
 	int flags;
 	struct os_reltime now;
+	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 "