@@ -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) {
@@ -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.
@@ -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);
@@ -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;
@@ -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;
@@ -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
@@ -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 "