Implements X.509 client authentication

This commit is contained in:
Simone Mainardi 2019-01-09 12:21:56 +01:00
parent 22e5e2e9e0
commit e9811ae688
10 changed files with 112 additions and 89 deletions

View file

@ -399,6 +399,7 @@ class Ntop {
bool changeAllowedIfname(char *username, char *allowed_ifname) const;
bool changeUserHostPool(const char * const username, const char * const host_pool_id) const;
bool changeUserLanguage(const char * const username, const char * const language) const;
bool existsUser(const char * const username) const;
bool addUser(char *username, char *full_name, char *password, char *host_role,
char *allowed_networks, char *allowed_ifname, char *host_pool_id,
char *language);

View file

@ -42,7 +42,7 @@ class Prefs {
char **deferred_interfaces_to_register, *cli;
char *http_binding_address1, *http_binding_address2;
char *https_binding_address1, *https_binding_address2;
bool enable_https_auth;
bool enable_client_x509_auth;
char *lan_interface;
Ntop *ntop;
bool enable_dns_resolution, sniff_dns_responses,
@ -216,10 +216,10 @@ class Prefs {
inline bool do_auto_logout_at_runtime() { return(enable_auto_logout_at_runtime); };
inline bool do_ignore_vlans() { return(ignore_vlans); };
inline bool do_simulate_vlans() { return(simulate_vlans); };
inline char* get_cpu_affinity() { return(cpu_affinity); };
inline u_int get_http_port() { return(http_port); };
inline u_int get_https_port() { return(https_port); };
inline bool is_https_auth() { return(enable_https_auth); };
inline char* get_cpu_affinity() { return(cpu_affinity); };
inline u_int get_http_port() { return(http_port); };
inline u_int get_https_port() { return(https_port); };
inline bool is_client_x509_auth_enabled() { return(enable_client_x509_auth); };
inline char* get_redis_host() { return(redis_host); }
inline char* get_redis_password() { return(redis_password); }
inline u_int get_redis_port() { return(redis_port); };

View file

@ -532,7 +532,7 @@
#define HOST_POOL_SHAPERS_KEY NTOPNG_PREFS_PREFIX".%u.l7_policies.%s"
#define HOST_POOL_DETAILS_KEY NTOPNG_PREFS_PREFIX".%u.host_pools.details.%u"
#define CONST_PREFS_AUTH_HTTPS NTOPNG_PREFS_PREFIX".is_https_auth"
#define CONST_PREFS_CLIENT_X509_AUTH NTOPNG_PREFS_PREFIX".is_client_x509_auth_enabled"
#define CONST_USER_GROUP_ADMIN "administrator"
#define CONST_USER_GROUP_UNPRIVILEGED "unprivileged"

View file

@ -2247,9 +2247,9 @@ local lang = {
["http_auth"] = "HTTP Authentication",
["http_auth_server_description"] = "The URL of the HTTP server to use to authenticate users.",
["http_auth_server_title"] = "HTTP Server",
["https_auth"] = "HTTPS Client Authentication",
["https_auth_description"] = "Settings will have effect at the next ntopng startup.",
["https_auth_title"] = "Toggle HTTPS Client Certificate Authentication",
["client_x509_auth"] = "HTTPS Client Authentication",
["client_x509_auth_descr"] = "Toggle the authentication of clients on the basis of their X.509 certificate. When the client X.509 Common Name (CN) equals one of the ntopng users, then the client is authenticated as the ntopng user. Changes to this setting requires an ntopng restart.",
["client_x509_auth_title"] = "Client X.509 Certificate Authentication",
["idle_timeout_settings"] = "Idle Timeout Settings",
["ignored_interfaces_description"] = "When 'Ingress Interface' is selected as disaggregation criterion, do not add to a dynamic interface any flow having its %%INPUT_SNMP in this list. When 'Interface' disaggregation is selected, do not add to the first (second) dynamic interface any flow having its %%INPUT_SNMP (%%OUTPUT_SNMP) in this list. Specify one or more comma-separated SNMP interface ids. Changes require %{product} restart.",
["ignored_interfaces_title"] = "Ignored interfaces",

View file

@ -1087,11 +1087,11 @@ function printAuthentication()
local entries = subpage_active.entries
-- Note: order must correspond to evaluation order in Ntop.cpp
print('<tr><th class="info" colspan="2">'..i18n("prefs.https_auth")..'</th></tr>')
print('<tr><th class="info" colspan="2">'..i18n("prefs.client_x509_auth")..'</th></tr>')
prefsToggleButton(subpage_active,{
field = "toggle_https_auth",
field = "toggle_client_x509_auth",
default = "0",
pref = "is_https_auth",
pref = "is_client_x509_auth_enabled",
})
if not entries.toggle_ldap_auth.hidden then
printLdapAuth()

View file

@ -1097,7 +1097,7 @@ local known_parameters = {
["toggle_radius_auth"] = validateBool,
["toggle_http_auth"] = validateBool,
["toggle_ldap_referrals"] = validateBool,
["toggle_https_auth"] = validateBool,
["toggle_client_x509_auth"] = validateBool,
-- Input fields
["minute_top_talkers_retention"] = validateNumber,

View file

@ -89,9 +89,9 @@ local menu_subpages = {
title = i18n("prefs.http_auth_server_title"),
description = i18n("prefs.http_auth_server_description"),
hidden = have_nedge,
}, toggle_https_auth = {
title = i18n("prefs.https_auth_title"),
description = i18n("prefs.https_auth_description"),
}, toggle_client_x509_auth = {
title = i18n("prefs.client_x509_auth_title"),
description = i18n("prefs.client_x509_auth_descr"),
},
}}, {id="ifaces", label=i18n("prefs.network_interfaces"), advanced=true, pro_only=false, hidden=false, nedge_hidden=true, entries={
dynamic_interfaces_creation = {

View file

@ -326,6 +326,48 @@ static int isWhitelistedURI(const char * const uri) {
/* ****************************************** */
static bool ssl_client_x509_auth(const struct mg_connection * const conn, const struct mg_request_info * const request_info,
char * const username, char * const group, bool * const localuser) {
bool ret = false;
X509 *cert = NULL;
X509_NAME *subj = NULL;
char subject[256];
char key[CONST_MAX_LEN_REDIS_KEY];
if((cert = SSL_get_peer_certificate(conn->ssl))) {
if((subj = X509_get_subject_name(cert))) {
X509_NAME_oneline(subj, subject, sizeof(subject));
if(SSL_get_verify_result(conn->ssl) == X509_V_OK
&& X509_NAME_get_text_by_NID(subj, NID_commonName, username, NTOP_USERNAME_MAXLEN) >= 0) {
snprintf(key, sizeof(key), CONST_STR_USER_GROUP, username);
bool group_exists = ntop->getRedis()->get(key, group, NTOP_GROUP_MAXLEN) >= 0;
if(ntop->existsUser(username)
&& group_exists) {
*localuser = true;
ntop->getTrace()->traceEvent(TRACE_INFO,"SSL user authenticated [username: %s][group: %s][subject: %s]", username, group, subject);
ret = true;
} else
ntop->getTrace()->traceEvent(TRACE_INFO,"SSL user: not found [user: %s]", username);
} else
ntop->getTrace()->traceEvent(TRACE_INFO,"SSL user: unknow certificate or missing NID_commonName [subject: %s]", subject);
}
X509_free(cert);
} else
ntop->getTrace()->traceEvent(TRACE_INFO,"SSL user: could not get certificate");
if(!ret)
username[0] = '\0', group[0] = '\0';
return ret;
};
/* ****************************************** */
// Return 1 if request is authorized, 0 otherwise.
// If 1 is returned, the username parameter will contain the authenticated user,
// which can also be "" or NTOP_NOLOGIN_USER .
@ -393,6 +435,12 @@ static int getAuthorizedUser(struct mg_connection *conn,
return(1);
}
/* Try to authenticate using client TLS/SSL certificate */
if(request_info->is_ssl
&& ntop->getPrefs()->is_client_x509_auth_enabled()
&& ssl_client_x509_auth(conn, request_info, username, group, localuser))
return(1);
/* Try to decode Authorization header if present */
auth_header_p = mg_get_header(conn, "Authorization");
string auth_header = auth_header_p ? auth_header_p : "";
@ -662,41 +710,6 @@ static void redirect_to_password_change(struct mg_connection *conn,
/* ****************************************** */
static int ssl_authorize(const struct mg_connection * const conn, const struct mg_request_info * const request_info, char * const group) {
long r;
X509 *cert = NULL;
X509_NAME *subj = NULL;
char subject[256], user[NTOP_USERNAME_MAXLEN];
char key[CONST_MAX_LEN_REDIS_KEY], val[NTOP_GROUP_MAXLEN];
if((cert = SSL_get_peer_certificate(conn->ssl))) {
if((subj = X509_get_subject_name(cert))) {
X509_NAME_oneline(subj, subject, sizeof(subject));
if((r = SSL_get_verify_result(conn->ssl)) == X509_V_OK) {
X509_NAME_get_text_by_NID(subj, NID_commonName, user, sizeof(user));
ntop->getTrace()->traceEvent(TRACE_NORMAL,"SSL user %s auth (%s)", user, subject);
snprintf(key, sizeof(key), CONST_STR_USER_GROUP, user);
strncpy(group, ((ntop->getRedis()->get(key, val, sizeof(val))>= 0) ? val : NTOP_UNKNOWN_GROUP), NTOP_GROUP_MAXLEN);
set_cookie(conn, user, group, true /* local user */, mg_get_header(conn, "Referer"));
X509_free(cert);
return 1;
} else
ntop->getTrace()->traceEvent(TRACE_NORMAL,"SSL user: unknow certificate (err %i) %s",r,subject);
}
X509_free(cert);
} else
ntop->getTrace()->traceEvent(TRACE_NORMAL,"SSL user: could not get certificate");
return 0;
};
/* ****************************************** */
// A handler for the /authorize endpoint.
// Login page form sends user name and password to this endpoint.
static void authorize(struct mg_connection *conn,
@ -878,38 +891,33 @@ static int handle_lua_request(struct mg_connection *conn) {
}
#endif
/* Make sure there are existing interfaces for username. */
if(!ntop->checkUserInterfaces(username)) {
char session_id[NTOP_SESSION_ID_LENGTH];
mg_get_cookie(conn, "session", session_id, sizeof(session_id));
ntop->getTrace()->traceEvent(TRACE_WARNING, "[HTTP] user %s cannot login due to non-existent allowed_interface", username);
// send error and expire session cookie
mg_printf(conn,
"HTTP/1.1 403 Forbidden\r\n"
"Content-Type: text/html\r\n"
"Set-Cookie: session=%s; path=/; expires=Thu, 01-Jan-1970 00:00:01 GMT; max-age=0;%s\r\n" // Session ID
"Connection: close\r\n"
"\r\n\r\n%s", session_id,
get_secure_cookie_attributes(request_info),
ACCESS_DENIED_INTERFACES);
return(1);
}
whitelisted = isWhitelistedURI(request_info->uri);
if(!isStaticResourceUrl(request_info, len)) {
/* Only check authorized for non-static resources */
u_int8_t authorized = getAuthorizedUser(conn, request_info, username, group, &localuser);
if((!whitelisted) && (!authorized)) {
if(request_info->is_ssl) {
if(ssl_authorize(conn, request_info, group))
return 1;
}
/* Make sure there are existing interfaces for username. */
if(!ntop->checkUserInterfaces(username)) {
char session_id[NTOP_SESSION_ID_LENGTH];
mg_get_cookie(conn, "session", session_id, sizeof(session_id));
ntop->getTrace()->traceEvent(TRACE_WARNING, "[HTTP] user %s cannot login due to non-existent allowed_interface", username);
// send error and expire session cookie
mg_printf(conn,
"HTTP/1.1 403 Forbidden\r\n"
"Content-Type: text/html\r\n"
"Set-Cookie: session=%s; path=/; expires=Thu, 01-Jan-1970 00:00:01 GMT; max-age=0;%s\r\n" // Session ID
"Connection: close\r\n"
"\r\n\r\n%s", session_id,
get_secure_cookie_attributes(request_info),
ACCESS_DENIED_INTERFACES);
return(1);
}
if((!whitelisted) && (!authorized)) {
if(strcmp(request_info->uri, NETWORK_LOAD_URL) == 0) {
// avoid sending login redirect to allow js itself to redirect the user
return(send_error(conn, 403 /* Forbidden */, request_info->uri, "Login Required"));
@ -1120,7 +1128,7 @@ void HTTPserver::parseACL(char * const acl, u_int acl_len) {
/* ****************************************** */
// Forse non necessaria???
// Not necessary?
int handle_ssl_verify(int ok, X509_STORE_CTX *ctx) {
X509 *cert;
char buf[256];
@ -1141,7 +1149,7 @@ static int ssl_session_ctx_id = 1;
/* ****************************************** */
int handle_ssl_init(void *ctx) {
int init_client_x509_auth(void *ctx) {
char buf[256];
char ssl_ca_path[MAX_PATH];
char ssl_cert_path[MAX_PATH];
@ -1153,15 +1161,15 @@ int handle_ssl_init(void *ctx) {
ntop->fixPath(ssl_ca_path),
ntop->fixPath(ssl_cert_path);
ntop->getTrace()->traceEvent(TRACE_NORMAL, "SSL init [ssl_ca_path: %s][ssl_cert_path: %s]", ssl_ca_path, ssl_cert_path);
if(!SSL_CTX_set_session_id_context((SSL_CTX*)ctx, (unsigned char*)&ssl_session_ctx_id, sizeof(ssl_session_ctx_id))) {
ntop->getTrace()->traceEvent(TRACE_WARNING, "SSL session init failed: %s", ERR_reason_error_string(ERR_get_error()));
return 0;
}
if(!SSL_CTX_load_verify_locations((SSL_CTX*)ctx, ssl_ca_path, NULL)) {
ntop->getTrace()->traceEvent(TRACE_WARNING, "SSL load client CA from '%s' failed: %s",ssl_ca_path, ERR_reason_error_string(ERR_get_error()));
ntop->getTrace()->traceEvent(TRACE_WARNING, "SSL load client CA from '%s' failed: %s",
ssl_ca_path,
ERR_reason_error_string(ERR_get_error()));
return 0;
}
@ -1172,7 +1180,7 @@ int handle_ssl_init(void *ctx) {
if((certnames = SSL_load_client_CA_file(ssl_ca_path))) {
SSL_CTX_set_client_CA_list((SSL_CTX*)ctx, certnames);
if((certnames=SSL_CTX_get_client_CA_list((SSL_CTX*)ctx))) {
if((certnames = SSL_CTX_get_client_CA_list((SSL_CTX*)ctx))) {
for(int i = 0; i < sk_X509_NAME_num(certnames); i++) {
X509_NAME_oneline(sk_X509_NAME_value(certnames, i), buf, sizeof(buf));
ntop->getTrace()->traceEvent(TRACE_NORMAL, "SSL loaded CA #%i: %s", i, buf);
@ -1184,6 +1192,8 @@ int handle_ssl_init(void *ctx) {
SSL_CTX_set_verify((SSL_CTX*)ctx, SSL_VERIFY_PEER/*|SSL_VERIFY_FAIL_IF_NO_PEER_CERT|SSL_VERIFY_CLIENT_ONCE*/, handle_ssl_verify);
//SSL_CTX_set_session_cache_mode((SSL_CTX*)ctx,SSL_SESS_CACHE_OFF);
ntop->getTrace()->traceEvent(TRACE_NORMAL, "SSL init [ssl_ca_path: %s][ssl_cert_path: %s]", ssl_ca_path, ssl_cert_path);
return 1;
};
@ -1218,7 +1228,8 @@ HTTPserver::HTTPserver(const char *_docs_dir, const char *_scripts_dir) {
memset(&callbacks, 0, sizeof(callbacks));
callbacks.begin_request = handle_lua_request;
callbacks.log_message = handle_http_message;
if(ntop->getPrefs()->is_https_auth()) callbacks.init_ssl=handle_ssl_init;
if(ntop->getPrefs()->is_client_x509_auth_enabled())
callbacks.init_ssl = init_client_x509_auth;
/* Randomize data */
gettimeofday(&tv, NULL);

View file

@ -1585,19 +1585,30 @@ bool Ntop::changeUserLanguage(const char * const username, const char * const la
return(true);
}
/* ******************************************* */
bool Ntop::existsUser(const char * const username) const {
char key[CONST_MAX_LEN_REDIS_KEY], val[2] /* Don't care about the content */;
snprintf(key, sizeof(key), CONST_STR_USER_GROUP, username);
if(ntop->getRedis()->get(key, val, sizeof(val)) >= 0)
return(true); // user already exists
return(false);
}
/* ******************************************* */
bool Ntop::addUser(char *username, char *full_name, char *password, char *host_role,
char *allowed_networks, char *allowed_ifname, char *host_pool_id,
char *language) {
char key[64], val[64];
char key[CONST_MAX_LEN_REDIS_KEY];
char password_hash[33];
snprintf(key, sizeof(key), CONST_STR_USER_FULL_NAME, username);
if(ntop->getRedis()->get(key, val, sizeof(val)) >= 0)
return(false); // user already exists
if(existsUser(username))
return(false);
snprintf(key, sizeof(key), CONST_STR_USER_FULL_NAME, username);
ntop->getRedis()->set(key, full_name, 0);
snprintf(key, sizeof(key), CONST_STR_USER_GROUP, username);

View file

@ -80,7 +80,7 @@ Prefs::Prefs(Ntop *_ntop) {
http_binding_address2 = NULL;
https_binding_address1 = NULL; // CONST_ANY_ADDRESS;
https_binding_address2 = NULL;
enable_https_auth=false;
enable_client_x509_auth = false;
lan_interface = NULL;
cpu_affinity = NULL;
redis_host = strdup("127.0.0.1");
@ -632,7 +632,7 @@ void Prefs::reloadPrefsFromRedis() {
}
global_dns_forging_enabled = getDefaultBoolPrefsValue(CONST_PREFS_GLOBAL_DNS_FORGING_ENABLED, false);
enable_https_auth=getDefaultBoolPrefsValue(CONST_PREFS_AUTH_HTTPS,false);
enable_client_x509_auth = getDefaultBoolPrefsValue(CONST_PREFS_CLIENT_X509_AUTH, false);
setTraceLevelFromRedis();
refreshHostsAlertsPrefs();