OpenSSL: Add 'check_cert_subject' support for TLS server

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>
Signed-off-by: Jouni Malinen <j@w1.fi>
This commit is contained in:
Jared Bents 2019-02-28 13:39:50 -06:00 committed by Jouni Malinen
parent 0173423f41
commit 841205a1ce
16 changed files with 356 additions and 2 deletions

View file

@ -2525,6 +2525,16 @@ 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) {
if (!pos[0]) {
wpa_printf(MSG_ERROR, "Line %d: unknown check_cert_subject '%s'",
line, pos);
return 1;
}
os_free(bss->check_cert_subject);
bss->check_cert_subject = os_strdup(pos);
if (!bss->check_cert_subject)
return 1;
} else if (os_strcmp(buf, "check_crl") == 0) {
bss->check_crl = atoi(pos);
} else if (os_strcmp(buf, "check_crl_strict") == 0) {

View file

@ -946,6 +946,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.
#
# For example, check_cert_subject=C=US/O=XX/OU=ABC/OU=XYZ/CN=1234 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 certificate C=US/O=XX/OU=XYZ/OU=ABC/CN=1234), hostapd will reject the
# 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 can only be used as per the following example.
#
# For example, check_cert_subject=C=US/O=XX/OU=Production* and 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 client certificates matches
# 'OU' value in 'check_cert_subject' up to 'wildcard'.
#
# * (Allow all clients, 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.

View file

@ -595,6 +595,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);

View file

@ -392,6 +392,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;

View file

@ -233,6 +233,7 @@ int authsrv_init(struct hostapd_data *hapd)
hapd->conf->ocsp_stapling_response;
params.ocsp_stapling_response_multi =
hapd->conf->ocsp_stapling_response_multi;
params.check_cert_subject = hapd->conf->check_cert_subject;
if (tls_global_set_params(hapd->ssl_ctx, &params)) {
wpa_printf(MSG_ERROR, "Failed to set TLS parameters");

View file

@ -42,6 +42,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,
};
@ -156,6 +157,7 @@ struct tls_config {
* @ocsp_stapling_response_multi: DER encoded file with cached OCSP stapling
* response list (OCSPResponseList for ocsp_multi in RFC 6961) or %NULL if
* ocsp_multi is not enabled
* @check_cert_subject: Client certificate subject name matching string
*
* TLS connection parameters to be configured with tls_connection_set_params()
* and tls_global_set_params().
@ -198,6 +200,7 @@ struct tls_connection_params {
unsigned int flags;
const char *ocsp_stapling_response;
const char *ocsp_stapling_response_multi;
const char *check_cert_subject;
};

View file

@ -739,6 +739,9 @@ int tls_global_set_params(void *tls_ctx,
struct tls_global *global = tls_ctx;
int ret;
if (params->check_cert_subject)
return -1; /* not yet supported */
/* Currently, global parameters are only set when running in server
* mode. */
global->server = 1;

View file

@ -309,6 +309,9 @@ int tls_global_set_params(void *tls_ctx,
struct tls_global *global = tls_ctx;
struct tlsv1_credentials *cred;
if (params->check_cert_subject)
return -1; /* not yet supported */
/* Currently, global parameters are only set when running in server
* mode. */
global->server = 1;

View file

@ -219,6 +219,7 @@ struct tls_data {
char *ca_cert;
unsigned int crl_reload_interval;
struct os_reltime crl_last_reload;
char *check_cert_subject;
};
struct tls_connection {
@ -232,6 +233,7 @@ struct tls_connection {
EVP_PKEY *private_key; /* the private key if using engine */
#endif /* OPENSSL_NO_ENGINE */
char *subject_match, *altsubject_match, *suffix_match, *domain_match;
char *check_cert_subject;
int read_alerts, write_alerts, failed;
tls_session_ticket_cb session_ticket_cb;
@ -1134,6 +1136,7 @@ void tls_deinit(void *ssl_ctx)
tls_global = NULL;
}
os_free(data->check_cert_subject);
os_free(data);
}
@ -1611,6 +1614,7 @@ void tls_connection_deinit(void *ssl_ctx, struct tls_connection *conn)
os_free(conn->altsubject_match);
os_free(conn->suffix_match);
os_free(conn->domain_match);
os_free(conn->check_cert_subject);
os_free(conn->session_ticket);
os_free(conn);
}
@ -1763,6 +1767,219 @@ static int domain_suffix_match(const u8 *val, size_t len, const char *match,
#endif /* CONFIG_NATIVE_WINDOWS */
struct tls_dn_field_order_cnt {
u8 cn;
u8 c;
u8 l;
u8 st;
u8 o;
u8 ou;
u8 email;
};
static int get_dn_field_index(const struct tls_dn_field_order_cnt *dn_cnt,
int nid)
{
switch (nid) {
case NID_commonName:
return dn_cnt->cn;
case NID_countryName:
return dn_cnt->c;
case NID_localityName:
return dn_cnt->l;
case NID_stateOrProvinceName:
return dn_cnt->st;
case NID_organizationName:
return dn_cnt->o;
case NID_organizationalUnitName:
return dn_cnt->ou;
case NID_pkcs9_emailAddress:
return dn_cnt->email;
default:
wpa_printf(MSG_ERROR,
"TLS: Unknown NID '%d' in check_cert_subject",
nid);
return -1;
}
}
/**
* match_dn_field - Match configuration DN field against Certificate DN field
* @cert: Certificate
* @nid: NID of DN field
* @field: Field name
* @value DN field value which is passed from configuration
* e.g., if configuration have C=US and this argument will point to US.
* @dn_cnt: DN matching context
* Returns: 1 on success and 0 on failure
*/
static int match_dn_field(const X509 *cert, int nid, const char *field,
const char *value,
const struct tls_dn_field_order_cnt *dn_cnt)
{
int i, ret = 0, len, config_dn_field_index, 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 */
config_dn_field_index = get_dn_field_index(dn_cnt, nid);
if (config_dn_field_index < 0)
return 0;
/* Fetch value based on NID */
for (i = -1; (i = X509_NAME_get_index_by_NID(name, nid, i)) > -1;) {
X509_NAME_ENTRY *e;
ASN1_STRING *cn;
e = X509_NAME_get_entry(name, i);
if (!e)
continue;
cn = X509_NAME_ENTRY_get_data(e);
if (!cn)
continue;
match_index++;
/* check for more than one DN field with same name */
if (match_index != config_dn_field_index)
continue;
/* Check wildcard at the right end side */
/* E.g., if OU=develop* mentioned in configuration, allow 'OU'
* of the subject in the client certificate to start with
* 'develop' */
if (len > 0 && value[len - 1] == '*') {
/* Compare actual certificate DN field value with
* configuration DN field value up to the specified
* length. */
ret = ASN1_STRING_length(cn) >= len - 1 &&
os_memcmp(ASN1_STRING_get0_data(cn), value,
len - 1) == 0;
} else {
/* Compare actual certificate DN field value with
* configuration DN field value */
ret = ASN1_STRING_length(cn) == len &&
os_memcmp(ASN1_STRING_get0_data(cn), value,
len) == 0;
}
if (!ret) {
wpa_printf(MSG_ERROR,
"OpenSSL: Failed to match %s '%s' with certificate DN field value '%s'",
field, value, ASN1_STRING_get0_data(cn));
}
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)
* @dn_cnt: DN matching context
* Returns: 1 on success and 0 on failure
*/
static int get_value_from_field(const X509 *cert, char *field_str,
struct tls_dn_field_order_cnt *dn_cnt)
{
int nid;
char *context = NULL, *name, *value;
if (os_strcmp(field_str, "*") == 0)
return 1; /* wildcard matches everything */
name = str_token(field_str, "=", &context);
if (!name)
return 0;
/* Compare all configured DN fields and assign nid based on that to
* fetch correct value from certificate subject */
if (os_strcmp(name, "CN") == 0) {
nid = NID_commonName;
dn_cnt->cn++;
} else if(os_strcmp(name, "C") == 0) {
nid = NID_countryName;
dn_cnt->c++;
} else if (os_strcmp(name, "L") == 0) {
nid = NID_localityName;
dn_cnt->l++;
} else if (os_strcmp(name, "ST") == 0) {
nid = NID_stateOrProvinceName;
dn_cnt->st++;
} else if (os_strcmp(name, "O") == 0) {
nid = NID_organizationName;
dn_cnt->o++;
} else if (os_strcmp(name, "OU") == 0) {
nid = NID_organizationalUnitName;
dn_cnt->ou++;
} else if (os_strcmp(name, "emailAddress") == 0) {
nid = NID_pkcs9_emailAddress;
dn_cnt->email++;
} else {
wpa_printf(MSG_ERROR,
"TLS: Unknown field '%s' in check_cert_subject", name);
return 0;
}
value = str_token(field_str, "=", &context);
if (!value) {
wpa_printf(MSG_ERROR,
"TLS: Distinguished Name field '%s' value is not defined in check_cert_subject",
name);
return 0;
}
return match_dn_field(cert, nid, name, value, dn_cnt);
}
/**
* tls_match_dn_field - Match subject DN 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)
{
const char *token, *last = NULL;
char field[256];
struct tls_dn_field_order_cnt dn_cnt;
os_memset(&dn_cnt, 0, sizeof(dn_cnt));
/* Maximum length of each DN field is 255 characters */
/* Process each '/' delimited field */
while ((token = cstr_token(match, "/", &last))) {
if (last - token >= (int) sizeof(field)) {
wpa_printf(MSG_ERROR,
"OpenSSL: Too long DN matching field value in '%s'",
match);
return 0;
}
os_memcpy(field, token, last - token);
field[last - token] = '\0';
if (!get_value_from_field(cert, field, &dn_cnt)) {
wpa_printf(MSG_DEBUG, "OpenSSL: No match for DN '%s'",
field);
return 0;
}
}
return 1;
}
static int tls_match_suffix(X509 *cert, const char *match, int full)
{
#ifdef CONFIG_NATIVE_WINDOWS
@ -2027,6 +2244,7 @@ static int tls_verify_cb(int preverify_ok, X509_STORE_CTX *x509_ctx)
struct tls_connection *conn;
struct tls_context *context;
char *match, *altmatch, *suffix_match, *domain_match;
const char *check_cert_subject;
const char *err_str;
err_cert = X509_STORE_CTX_get_current_cert(x509_ctx);
@ -2127,6 +2345,18 @@ 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);
check_cert_subject = conn->check_cert_subject;
if (!check_cert_subject)
check_cert_subject = conn->data->check_cert_subject;
if (check_cert_subject) {
if (depth == 0 &&
!tls_match_dn_field(err_cert, 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);
@ -2503,7 +2733,8 @@ static int tls_connection_set_subject_match(struct tls_connection *conn,
const char *subject_match,
const char *altsubject_match,
const char *suffix_match,
const char *domain_match)
const char *domain_match,
const char *check_cert_subject)
{
os_free(conn->subject_match);
conn->subject_match = NULL;
@ -2537,6 +2768,14 @@ static int tls_connection_set_subject_match(struct tls_connection *conn,
return -1;
}
os_free(conn->check_cert_subject);
conn->check_cert_subject = NULL;
if (check_cert_subject) {
conn->check_cert_subject = os_strdup(check_cert_subject);
if (!conn->check_cert_subject)
return -1;
}
return 0;
}
@ -4591,7 +4830,8 @@ int tls_connection_set_params(void *tls_ctx, struct tls_connection *conn,
params->subject_match,
params->altsubject_match,
params->suffix_match,
params->domain_match))
params->domain_match,
params->check_cert_subject))
return -1;
if (engine_id && ca_cert_id) {
@ -4732,6 +4972,15 @@ int tls_global_set_params(void *tls_ctx,
__func__, ERR_error_string(err, NULL));
}
os_free(data->check_cert_subject);
data->check_cert_subject = NULL;
if (params->check_cert_subject) {
data->check_cert_subject =
os_strdup(params->check_cert_subject);
if (!data->check_cert_subject)
return -1;
}
if (tls_global_ca_cert(data, params->ca_cert) ||
tls_global_client_cert(data, params->client_cert) ||
tls_global_private_key(data, params->private_key,

View file

@ -1487,6 +1487,9 @@ int tls_global_set_params(void *tls_ctx,
{
wpa_printf(MSG_DEBUG, "SSL: global set params");
if (params->check_cert_subject)
return -1; /* not yet supported */
if (tls_global_ca_cert(tls_ctx, params->ca_cert) < 0) {
wpa_printf(MSG_INFO, "SSL: Failed to load ca cert file '%s'",
params->ca_cert);

View file

@ -196,6 +196,48 @@ struct eap_peer_config {
*/
char *subject_match;
/**
* check_cert_subject - Constraint for server certificate subject fields
*
* If check_cert_subject is set, the value of every field will be
* checked against the DN of the subject in the authentication server
* certificate. If the values do not match, the certificate verification
* will fail, rejecting the server. This option allows wpa_supplicant to
* match every individual field in the right order against the DN of the
* subject in the server certificate.
*
* For example, check_cert_subject=C=US/O=XX/OU=ABC/OU=XYZ/CN=1234 will
* check every individual DN field of the subject in the server
* certificate. If OU=XYZ comes first in terms of the order in the
* server certificate (DN field of server certificate
* C=US/O=XX/OU=XYZ/OU=ABC/CN=1234), wpa_supplicant will reject the
* server 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 can only be used as per the following example.
*
* For example, check_cert_subject=C=US/O=XX/OU=Production* and we have
* two servers and DN of the subject in the first server certificate is
* (C=US/O=XX/OU=Production Unit) and DN of the subject in the second
* server is (C=US/O=XX/OU=Production Factory). In this case,
* wpa_supplicant will allow both servers because the value of 'OU'
* field in both server certificates matches 'OU' value in
* 'check_cert_subject' up to 'wildcard'.
*
* (Allow all servers, e.g., check_cert_subject=*)
*/
char *check_cert_subject;
/**
* check_cert_subject2 - Constraint for server certificate subject fields
*
* This field is like check_cert_subject, but used for phase 2 (inside
* EAP-TTLS/PEAP/FAST tunnel) authentication.
*/
char *check_cert_subject2;
/**
* altsubject_match - Constraint for server certificate alt. subject
*

View file

@ -116,6 +116,7 @@ static void eap_tls_params_from_conf1(struct tls_connection_params *params,
params->dh_file = config->dh_file;
params->subject_match = config->subject_match;
params->altsubject_match = config->altsubject_match;
params->check_cert_subject = config->check_cert_subject;
params->suffix_match = config->domain_suffix_match;
params->domain_match = config->domain_match;
params->engine = config->engine;
@ -139,6 +140,7 @@ static void eap_tls_params_from_conf2(struct tls_connection_params *params,
params->dh_file = config->dh_file2;
params->subject_match = config->subject_match2;
params->altsubject_match = config->altsubject_match2;
params->check_cert_subject = config->check_cert_subject2;
params->suffix_match = config->domain_suffix_match2;
params->domain_match = config->domain_match2;
params->engine = config->engine2;

View file

@ -2257,6 +2257,7 @@ static const struct parse_data ssid_fields[] = {
{ STR_KEYe(private_key_passwd) },
{ STRe(dh_file) },
{ STRe(subject_match) },
{ STRe(check_cert_subject) },
{ STRe(altsubject_match) },
{ STRe(domain_suffix_match) },
{ STRe(domain_match) },
@ -2267,6 +2268,7 @@ static const struct parse_data ssid_fields[] = {
{ STR_KEYe(private_key2_passwd) },
{ STRe(dh_file2) },
{ STRe(subject_match2) },
{ STRe(check_cert_subject2) },
{ STRe(altsubject_match2) },
{ STRe(domain_suffix_match2) },
{ STRe(domain_match2) },
@ -2525,6 +2527,7 @@ static void eap_peer_config_free(struct eap_peer_config *eap)
str_clear_free(eap->private_key_passwd);
os_free(eap->dh_file);
os_free(eap->subject_match);
os_free(eap->check_cert_subject);
os_free(eap->altsubject_match);
os_free(eap->domain_suffix_match);
os_free(eap->domain_match);
@ -2535,6 +2538,7 @@ static void eap_peer_config_free(struct eap_peer_config *eap)
str_clear_free(eap->private_key2_passwd);
os_free(eap->dh_file2);
os_free(eap->subject_match2);
os_free(eap->check_cert_subject2);
os_free(eap->altsubject_match2);
os_free(eap->domain_suffix_match2);
os_free(eap->domain_match2);

View file

@ -782,6 +782,7 @@ static void wpa_config_write_network(FILE *f, struct wpa_ssid *ssid)
STR(private_key_passwd);
STR(dh_file);
STR(subject_match);
STR(check_cert_subject);
STR(altsubject_match);
STR(domain_suffix_match);
STR(domain_match);
@ -792,6 +793,7 @@ static void wpa_config_write_network(FILE *f, struct wpa_ssid *ssid)
STR(private_key2_passwd);
STR(dh_file2);
STR(subject_match2);
STR(check_cert_subject2);
STR(altsubject_match2);
STR(domain_suffix_match2);
STR(domain_match2);

View file

@ -896,6 +896,7 @@ static int wpa_config_write_network(HKEY hk, struct wpa_ssid *ssid, int id)
STR(private_key_passwd);
STR(dh_file);
STR(subject_match);
STR(check_cert_subject);
STR(altsubject_match);
STR(ca_cert2);
STR(ca_path2);
@ -904,6 +905,7 @@ static int wpa_config_write_network(HKEY hk, struct wpa_ssid *ssid, int id)
STR(private_key2_passwd);
STR(dh_file2);
STR(subject_match2);
STR(check_cert_subject2);
STR(altsubject_match2);
STR(phase1);
STR(phase2);

View file

@ -1411,9 +1411,11 @@ static const char *network_fields[] = {
"eap", "identity", "anonymous_identity", "password", "ca_cert",
"ca_path", "client_cert", "private_key", "private_key_passwd",
"dh_file", "subject_match", "altsubject_match",
"check_cert_subject",
"domain_suffix_match", "domain_match", "ca_cert2", "ca_path2",
"client_cert2", "private_key2", "private_key2_passwd",
"dh_file2", "subject_match2", "altsubject_match2",
"check_cert_subject2",
"domain_suffix_match2", "domain_match2", "phase1", "phase2",
"pcsc", "pin", "engine_id", "key_id", "cert_id", "ca_cert_id",
"pin2", "engine2_id", "key2_id", "cert2_id", "ca_cert2_id",