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:
parent
0173423f41
commit
841205a1ce
16 changed files with 356 additions and 2 deletions
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue