Cumulative patch from commit 39b420f7b12b152f879d1bb8a5267b9a838e04dc

39b420f HS 2.0R2: Add parse_cert command for debugging purposes
c0d701a HS 2.0R2: Add OSU client implementation
d2bb2b4 Add os_file_exists()
1cb3eda HS 2.0R2: Add wrapper functions for libcurl
2cb8f96 HS 2.0R2: Add wrapper functions for libxml2
da4ec2c HS 2.0R2: Add wrapper for system browser
db3ca20 HS 2.0R2: Add wrapper for wpadebug browser on Android
2a10e57 HS 2.0R2: Add wrapper for Android browser for user interaction
d0b79d7 HS 2.0R2: Add wrapper functions for WebKit
91728f2 Add wpa_ctrl helper functions for upper level functionality
b4c26ef Clean up hostapd_config_fill() parsers
b2e32cd Fix memory leaks on wpa_config_parse_string() error paths
a0b728b Simplify hostapd_config_fill() error reporting
599f40d Remove extra indentation level from hostapd_config_fill()

Change-Id: I17b7089210da7e083c9fe5814e79cef99bbee80b
Signed-off-by: Dmitry Shmidt <dimitrysh@google.com>
diff --git a/hostapd/config_file.c b/hostapd/config_file.c
index b6f1d1b..f018f96 100644
--- a/hostapd/config_file.c
+++ b/hostapd/config_file.c
@@ -1679,6 +1679,7 @@
 	str = wpa_config_parse_string(pos, &slen);
 	if (str == NULL || slen < 1 || slen > HOSTAPD_MAX_SSID_LEN) {
 		wpa_printf(MSG_ERROR, "Line %d: Invalid SSID '%s'", line, pos);
+		os_free(str);
 		return -1;
 	}
 
@@ -1837,1389 +1838,1308 @@
 			       struct hostapd_bss_config *bss,
 			       char *buf, char *pos, int line)
 {
-	int errors = 0;
-
-	{
-		if (os_strcmp(buf, "interface") == 0) {
-			os_strlcpy(conf->bss[0]->iface, pos,
-				   sizeof(conf->bss[0]->iface));
-		} else if (os_strcmp(buf, "bridge") == 0) {
-			os_strlcpy(bss->bridge, pos, sizeof(bss->bridge));
-		} else if (os_strcmp(buf, "vlan_bridge") == 0) {
-			os_strlcpy(bss->vlan_bridge, pos,
-			           sizeof(bss->vlan_bridge));
-		} else if (os_strcmp(buf, "wds_bridge") == 0) {
-			os_strlcpy(bss->wds_bridge, pos,
-				   sizeof(bss->wds_bridge));
-		} else if (os_strcmp(buf, "driver") == 0) {
-			int j;
-			/* clear to get error below if setting is invalid */
-			conf->driver = NULL;
-			for (j = 0; wpa_drivers[j]; j++) {
-				if (os_strcmp(pos, wpa_drivers[j]->name) == 0)
-				{
-					conf->driver = wpa_drivers[j];
-					break;
-				}
+	if (os_strcmp(buf, "interface") == 0) {
+		os_strlcpy(conf->bss[0]->iface, pos,
+			   sizeof(conf->bss[0]->iface));
+	} else if (os_strcmp(buf, "bridge") == 0) {
+		os_strlcpy(bss->bridge, pos, sizeof(bss->bridge));
+	} else if (os_strcmp(buf, "vlan_bridge") == 0) {
+		os_strlcpy(bss->vlan_bridge, pos, sizeof(bss->vlan_bridge));
+	} else if (os_strcmp(buf, "wds_bridge") == 0) {
+		os_strlcpy(bss->wds_bridge, pos, sizeof(bss->wds_bridge));
+	} else if (os_strcmp(buf, "driver") == 0) {
+		int j;
+		/* clear to get error below if setting is invalid */
+		conf->driver = NULL;
+		for (j = 0; wpa_drivers[j]; j++) {
+			if (os_strcmp(pos, wpa_drivers[j]->name) == 0) {
+				conf->driver = wpa_drivers[j];
+				break;
 			}
-			if (conf->driver == NULL) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid/"
-					   "unknown driver '%s'", line, pos);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "debug") == 0) {
-			wpa_printf(MSG_DEBUG, "Line %d: DEPRECATED: 'debug' "
-				   "configuration variable is not used "
-				   "anymore", line);
-		} else if (os_strcmp(buf, "logger_syslog_level") == 0) {
-			bss->logger_syslog_level = atoi(pos);
-		} else if (os_strcmp(buf, "logger_stdout_level") == 0) {
-			bss->logger_stdout_level = atoi(pos);
-		} else if (os_strcmp(buf, "logger_syslog") == 0) {
-			bss->logger_syslog = atoi(pos);
-		} else if (os_strcmp(buf, "logger_stdout") == 0) {
-			bss->logger_stdout = atoi(pos);
-		} else if (os_strcmp(buf, "dump_file") == 0) {
-			wpa_printf(MSG_INFO, "Line %d: DEPRECATED: 'dump_file' configuration variable is not used anymore",
-				   line);
-		} else if (os_strcmp(buf, "ssid") == 0) {
-			bss->ssid.ssid_len = os_strlen(pos);
-			if (bss->ssid.ssid_len > HOSTAPD_MAX_SSID_LEN ||
-			    bss->ssid.ssid_len < 1) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid SSID "
-					   "'%s'", line, pos);
-				errors++;
-			} else {
-				os_memcpy(bss->ssid.ssid, pos,
-					  bss->ssid.ssid_len);
-				bss->ssid.ssid_set = 1;
-			}
-		} else if (os_strcmp(buf, "ssid2") == 0) {
-			size_t slen;
-			char *str = wpa_config_parse_string(pos, &slen);
-			if (str == NULL || slen < 1 ||
-				   slen > HOSTAPD_MAX_SSID_LEN) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid SSID "
-					   "'%s'", line, pos);
-				errors++;
-			} else {
-				os_memcpy(bss->ssid.ssid, str, slen);
-				bss->ssid.ssid_len = slen;
-				bss->ssid.ssid_set = 1;
-			}
+		}
+		if (conf->driver == NULL) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: invalid/unknown driver '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "debug") == 0) {
+		wpa_printf(MSG_DEBUG, "Line %d: DEPRECATED: 'debug' configuration variable is not used anymore",
+			   line);
+	} else if (os_strcmp(buf, "logger_syslog_level") == 0) {
+		bss->logger_syslog_level = atoi(pos);
+	} else if (os_strcmp(buf, "logger_stdout_level") == 0) {
+		bss->logger_stdout_level = atoi(pos);
+	} else if (os_strcmp(buf, "logger_syslog") == 0) {
+		bss->logger_syslog = atoi(pos);
+	} else if (os_strcmp(buf, "logger_stdout") == 0) {
+		bss->logger_stdout = atoi(pos);
+	} else if (os_strcmp(buf, "dump_file") == 0) {
+		wpa_printf(MSG_INFO, "Line %d: DEPRECATED: 'dump_file' configuration variable is not used anymore",
+			   line);
+	} else if (os_strcmp(buf, "ssid") == 0) {
+		bss->ssid.ssid_len = os_strlen(pos);
+		if (bss->ssid.ssid_len > HOSTAPD_MAX_SSID_LEN ||
+		    bss->ssid.ssid_len < 1) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid SSID '%s'",
+				   line, pos);
+			return 1;
+		}
+		os_memcpy(bss->ssid.ssid, pos, bss->ssid.ssid_len);
+		bss->ssid.ssid_set = 1;
+	} else if (os_strcmp(buf, "ssid2") == 0) {
+		size_t slen;
+		char *str = wpa_config_parse_string(pos, &slen);
+		if (str == NULL || slen < 1 || slen > HOSTAPD_MAX_SSID_LEN) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid SSID '%s'",
+				   line, pos);
 			os_free(str);
-		} else if (os_strcmp(buf, "utf8_ssid") == 0) {
-			bss->ssid.utf8_ssid = atoi(pos) > 0;
-		} else if (os_strcmp(buf, "macaddr_acl") == 0) {
-			bss->macaddr_acl = atoi(pos);
-			if (bss->macaddr_acl != ACCEPT_UNLESS_DENIED &&
-			    bss->macaddr_acl != DENY_UNLESS_ACCEPTED &&
-			    bss->macaddr_acl != USE_EXTERNAL_RADIUS_AUTH) {
-				wpa_printf(MSG_ERROR, "Line %d: unknown "
-					   "macaddr_acl %d",
-					   line, bss->macaddr_acl);
-			}
-		} else if (os_strcmp(buf, "accept_mac_file") == 0) {
-			if (hostapd_config_read_maclist(pos, &bss->accept_mac,
-							&bss->num_accept_mac))
-			{
-				wpa_printf(MSG_ERROR, "Line %d: Failed to "
-					   "read accept_mac_file '%s'",
-					   line, pos);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "deny_mac_file") == 0) {
-			if (hostapd_config_read_maclist(pos, &bss->deny_mac,
-							&bss->num_deny_mac)) {
-				wpa_printf(MSG_ERROR, "Line %d: Failed to "
-					   "read deny_mac_file '%s'",
-					   line, pos);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "wds_sta") == 0) {
-			bss->wds_sta = atoi(pos);
-		} else if (os_strcmp(buf, "start_disabled") == 0) {
-			bss->start_disabled = atoi(pos);
-		} else if (os_strcmp(buf, "ap_isolate") == 0) {
-			bss->isolate = atoi(pos);
-		} else if (os_strcmp(buf, "ap_max_inactivity") == 0) {
-			bss->ap_max_inactivity = atoi(pos);
-		} else if (os_strcmp(buf, "skip_inactivity_poll") == 0) {
-			bss->skip_inactivity_poll = atoi(pos);
-		} else if (os_strcmp(buf, "country_code") == 0) {
-			os_memcpy(conf->country, pos, 2);
-			/* FIX: make this configurable */
-			conf->country[2] = ' ';
-		} else if (os_strcmp(buf, "ieee80211d") == 0) {
-			conf->ieee80211d = atoi(pos);
-		} else if (os_strcmp(buf, "ieee80211h") == 0) {
-			conf->ieee80211h = atoi(pos);
-		} else if (os_strcmp(buf, "ieee8021x") == 0) {
-			bss->ieee802_1x = atoi(pos);
-		} else if (os_strcmp(buf, "eapol_version") == 0) {
-			bss->eapol_version = atoi(pos);
-			if (bss->eapol_version < 1 ||
-			    bss->eapol_version > 2) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid EAPOL "
-					   "version (%d): '%s'.",
-					   line, bss->eapol_version, pos);
-				errors++;
-			} else
-				wpa_printf(MSG_DEBUG, "eapol_version=%d",
-					   bss->eapol_version);
+			return 1;
+		}
+		os_memcpy(bss->ssid.ssid, str, slen);
+		bss->ssid.ssid_len = slen;
+		bss->ssid.ssid_set = 1;
+		os_free(str);
+	} else if (os_strcmp(buf, "utf8_ssid") == 0) {
+		bss->ssid.utf8_ssid = atoi(pos) > 0;
+	} else if (os_strcmp(buf, "macaddr_acl") == 0) {
+		bss->macaddr_acl = atoi(pos);
+		if (bss->macaddr_acl != ACCEPT_UNLESS_DENIED &&
+		    bss->macaddr_acl != DENY_UNLESS_ACCEPTED &&
+		    bss->macaddr_acl != USE_EXTERNAL_RADIUS_AUTH) {
+			wpa_printf(MSG_ERROR, "Line %d: unknown macaddr_acl %d",
+				   line, bss->macaddr_acl);
+		}
+	} else if (os_strcmp(buf, "accept_mac_file") == 0) {
+		if (hostapd_config_read_maclist(pos, &bss->accept_mac,
+						&bss->num_accept_mac)) {
+			wpa_printf(MSG_ERROR, "Line %d: Failed to read accept_mac_file '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "deny_mac_file") == 0) {
+		if (hostapd_config_read_maclist(pos, &bss->deny_mac,
+						&bss->num_deny_mac)) {
+			wpa_printf(MSG_ERROR, "Line %d: Failed to read deny_mac_file '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "wds_sta") == 0) {
+		bss->wds_sta = atoi(pos);
+	} else if (os_strcmp(buf, "start_disabled") == 0) {
+		bss->start_disabled = atoi(pos);
+	} else if (os_strcmp(buf, "ap_isolate") == 0) {
+		bss->isolate = atoi(pos);
+	} else if (os_strcmp(buf, "ap_max_inactivity") == 0) {
+		bss->ap_max_inactivity = atoi(pos);
+	} else if (os_strcmp(buf, "skip_inactivity_poll") == 0) {
+		bss->skip_inactivity_poll = atoi(pos);
+	} else if (os_strcmp(buf, "country_code") == 0) {
+		os_memcpy(conf->country, pos, 2);
+		/* FIX: make this configurable */
+		conf->country[2] = ' ';
+	} else if (os_strcmp(buf, "ieee80211d") == 0) {
+		conf->ieee80211d = atoi(pos);
+	} else if (os_strcmp(buf, "ieee80211h") == 0) {
+		conf->ieee80211h = atoi(pos);
+	} else if (os_strcmp(buf, "ieee8021x") == 0) {
+		bss->ieee802_1x = atoi(pos);
+	} else if (os_strcmp(buf, "eapol_version") == 0) {
+		bss->eapol_version = atoi(pos);
+		if (bss->eapol_version < 1 || bss->eapol_version > 2) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: invalid EAPOL version (%d): '%s'.",
+				   line, bss->eapol_version, pos);
+			return 1;
+		}
+		wpa_printf(MSG_DEBUG, "eapol_version=%d", bss->eapol_version);
 #ifdef EAP_SERVER
-		} else if (os_strcmp(buf, "eap_authenticator") == 0) {
-			bss->eap_server = atoi(pos);
-			wpa_printf(MSG_ERROR, "Line %d: obsolete "
-				   "eap_authenticator used; this has been "
-				   "renamed to eap_server", line);
-		} else if (os_strcmp(buf, "eap_server") == 0) {
-			bss->eap_server = atoi(pos);
-		} else if (os_strcmp(buf, "eap_user_file") == 0) {
-			if (hostapd_config_read_eap_user(pos, bss))
-				errors++;
-		} else if (os_strcmp(buf, "ca_cert") == 0) {
-			os_free(bss->ca_cert);
-			bss->ca_cert = os_strdup(pos);
-		} else if (os_strcmp(buf, "server_cert") == 0) {
-			os_free(bss->server_cert);
-			bss->server_cert = os_strdup(pos);
-		} else if (os_strcmp(buf, "private_key") == 0) {
-			os_free(bss->private_key);
-			bss->private_key = os_strdup(pos);
-		} 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_crl") == 0) {
-			bss->check_crl = atoi(pos);
-		} else if (os_strcmp(buf, "ocsp_stapling_response") == 0) {
-			os_free(bss->ocsp_stapling_response);
-			bss->ocsp_stapling_response = os_strdup(pos);
-		} else if (os_strcmp(buf, "dh_file") == 0) {
-			os_free(bss->dh_file);
-			bss->dh_file = os_strdup(pos);
-		} else if (os_strcmp(buf, "fragment_size") == 0) {
-			bss->fragment_size = atoi(pos);
+	} else if (os_strcmp(buf, "eap_authenticator") == 0) {
+		bss->eap_server = atoi(pos);
+		wpa_printf(MSG_ERROR, "Line %d: obsolete eap_authenticator used; this has been renamed to eap_server", line);
+	} else if (os_strcmp(buf, "eap_server") == 0) {
+		bss->eap_server = atoi(pos);
+	} else if (os_strcmp(buf, "eap_user_file") == 0) {
+		if (hostapd_config_read_eap_user(pos, bss))
+			return 1;
+	} else if (os_strcmp(buf, "ca_cert") == 0) {
+		os_free(bss->ca_cert);
+		bss->ca_cert = os_strdup(pos);
+	} else if (os_strcmp(buf, "server_cert") == 0) {
+		os_free(bss->server_cert);
+		bss->server_cert = os_strdup(pos);
+	} else if (os_strcmp(buf, "private_key") == 0) {
+		os_free(bss->private_key);
+		bss->private_key = os_strdup(pos);
+	} 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_crl") == 0) {
+		bss->check_crl = atoi(pos);
+	} else if (os_strcmp(buf, "ocsp_stapling_response") == 0) {
+		os_free(bss->ocsp_stapling_response);
+		bss->ocsp_stapling_response = os_strdup(pos);
+	} else if (os_strcmp(buf, "dh_file") == 0) {
+		os_free(bss->dh_file);
+		bss->dh_file = os_strdup(pos);
+	} else if (os_strcmp(buf, "fragment_size") == 0) {
+		bss->fragment_size = atoi(pos);
 #ifdef EAP_SERVER_FAST
-		} else if (os_strcmp(buf, "pac_opaque_encr_key") == 0) {
-			os_free(bss->pac_opaque_encr_key);
-			bss->pac_opaque_encr_key = os_malloc(16);
-			if (bss->pac_opaque_encr_key == NULL) {
-				wpa_printf(MSG_ERROR, "Line %d: No memory for "
-					   "pac_opaque_encr_key", line);
-				errors++;
-			} else if (hexstr2bin(pos, bss->pac_opaque_encr_key,
-					      16)) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "pac_opaque_encr_key", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "eap_fast_a_id") == 0) {
-			size_t idlen = os_strlen(pos);
-			if (idlen & 1) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "eap_fast_a_id", line);
-				errors++;
-			} else {
-				os_free(bss->eap_fast_a_id);
-				bss->eap_fast_a_id = os_malloc(idlen / 2);
-				if (bss->eap_fast_a_id == NULL ||
-				    hexstr2bin(pos, bss->eap_fast_a_id,
-					       idlen / 2)) {
-					wpa_printf(MSG_ERROR, "Line %d: "
-						   "Failed to parse "
-						   "eap_fast_a_id", line);
-					errors++;
-				} else
-					bss->eap_fast_a_id_len = idlen / 2;
-			}
-		} else if (os_strcmp(buf, "eap_fast_a_id_info") == 0) {
-			os_free(bss->eap_fast_a_id_info);
-			bss->eap_fast_a_id_info = os_strdup(pos);
-		} else if (os_strcmp(buf, "eap_fast_prov") == 0) {
-			bss->eap_fast_prov = atoi(pos);
-		} else if (os_strcmp(buf, "pac_key_lifetime") == 0) {
-			bss->pac_key_lifetime = atoi(pos);
-		} else if (os_strcmp(buf, "pac_key_refresh_time") == 0) {
-			bss->pac_key_refresh_time = atoi(pos);
+	} else if (os_strcmp(buf, "pac_opaque_encr_key") == 0) {
+		os_free(bss->pac_opaque_encr_key);
+		bss->pac_opaque_encr_key = os_malloc(16);
+		if (bss->pac_opaque_encr_key == NULL) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: No memory for pac_opaque_encr_key",
+				   line);
+			return 1;
+		} else if (hexstr2bin(pos, bss->pac_opaque_encr_key, 16)) {
+			wpa_printf(MSG_ERROR, "Line %d: Invalid pac_opaque_encr_key",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "eap_fast_a_id") == 0) {
+		size_t idlen = os_strlen(pos);
+		if (idlen & 1) {
+			wpa_printf(MSG_ERROR, "Line %d: Invalid eap_fast_a_id",
+				   line);
+			return 1;
+		}
+		os_free(bss->eap_fast_a_id);
+		bss->eap_fast_a_id = os_malloc(idlen / 2);
+		if (bss->eap_fast_a_id == NULL ||
+		    hexstr2bin(pos, bss->eap_fast_a_id, idlen / 2)) {
+			wpa_printf(MSG_ERROR, "Line %d: Failed to parse eap_fast_a_id",
+				   line);
+			os_free(bss->eap_fast_a_id);
+			bss->eap_fast_a_id = NULL;
+			return 1;
+		} else {
+			bss->eap_fast_a_id_len = idlen / 2;
+		}
+	} else if (os_strcmp(buf, "eap_fast_a_id_info") == 0) {
+		os_free(bss->eap_fast_a_id_info);
+		bss->eap_fast_a_id_info = os_strdup(pos);
+	} else if (os_strcmp(buf, "eap_fast_prov") == 0) {
+		bss->eap_fast_prov = atoi(pos);
+	} else if (os_strcmp(buf, "pac_key_lifetime") == 0) {
+		bss->pac_key_lifetime = atoi(pos);
+	} else if (os_strcmp(buf, "pac_key_refresh_time") == 0) {
+		bss->pac_key_refresh_time = atoi(pos);
 #endif /* EAP_SERVER_FAST */
 #ifdef EAP_SERVER_SIM
-		} else if (os_strcmp(buf, "eap_sim_db") == 0) {
-			os_free(bss->eap_sim_db);
-			bss->eap_sim_db = os_strdup(pos);
-		} else if (os_strcmp(buf, "eap_sim_aka_result_ind") == 0) {
-			bss->eap_sim_aka_result_ind = atoi(pos);
+	} else if (os_strcmp(buf, "eap_sim_db") == 0) {
+		os_free(bss->eap_sim_db);
+		bss->eap_sim_db = os_strdup(pos);
+	} else if (os_strcmp(buf, "eap_sim_aka_result_ind") == 0) {
+		bss->eap_sim_aka_result_ind = atoi(pos);
 #endif /* EAP_SERVER_SIM */
 #ifdef EAP_SERVER_TNC
-		} else if (os_strcmp(buf, "tnc") == 0) {
-			bss->tnc = atoi(pos);
+	} else if (os_strcmp(buf, "tnc") == 0) {
+		bss->tnc = atoi(pos);
 #endif /* EAP_SERVER_TNC */
 #ifdef EAP_SERVER_PWD
-		} else if (os_strcmp(buf, "pwd_group") == 0) {
-			bss->pwd_group = atoi(pos);
+	} else if (os_strcmp(buf, "pwd_group") == 0) {
+		bss->pwd_group = atoi(pos);
 #endif /* EAP_SERVER_PWD */
 #endif /* EAP_SERVER */
-		} else if (os_strcmp(buf, "eap_message") == 0) {
-			char *term;
-			bss->eap_req_id_text = os_strdup(pos);
-			if (bss->eap_req_id_text == NULL) {
-				wpa_printf(MSG_ERROR, "Line %d: Failed to "
-					   "allocate memory for "
-					   "eap_req_id_text", line);
-				errors++;
-				return errors;
-			}
-			bss->eap_req_id_text_len =
-				os_strlen(bss->eap_req_id_text);
-			term = os_strstr(bss->eap_req_id_text, "\\0");
-			if (term) {
-				*term++ = '\0';
-				os_memmove(term, term + 1,
-					   bss->eap_req_id_text_len -
-					   (term - bss->eap_req_id_text) - 1);
-				bss->eap_req_id_text_len--;
-			}
-		} else if (os_strcmp(buf, "wep_key_len_broadcast") == 0) {
-			bss->default_wep_key_len = atoi(pos);
-			if (bss->default_wep_key_len > 13) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid WEP "
-					   "key len %lu (= %lu bits)", line,
-					   (unsigned long)
-					   bss->default_wep_key_len,
-					   (unsigned long)
-					   bss->default_wep_key_len * 8);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "wep_key_len_unicast") == 0) {
-			bss->individual_wep_key_len = atoi(pos);
-			if (bss->individual_wep_key_len < 0 ||
-			    bss->individual_wep_key_len > 13) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid WEP "
-					   "key len %d (= %d bits)", line,
-					   bss->individual_wep_key_len,
-					   bss->individual_wep_key_len * 8);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "wep_rekey_period") == 0) {
-			bss->wep_rekeying_period = atoi(pos);
-			if (bss->wep_rekeying_period < 0) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "period %d",
-					   line, bss->wep_rekeying_period);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "eap_reauth_period") == 0) {
-			bss->eap_reauth_period = atoi(pos);
-			if (bss->eap_reauth_period < 0) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "period %d",
-					   line, bss->eap_reauth_period);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "eapol_key_index_workaround") == 0) {
-			bss->eapol_key_index_workaround = atoi(pos);
+	} else if (os_strcmp(buf, "eap_message") == 0) {
+		char *term;
+		bss->eap_req_id_text = os_strdup(pos);
+		if (bss->eap_req_id_text == NULL) {
+			wpa_printf(MSG_ERROR, "Line %d: Failed to allocate memory for eap_req_id_text",
+				   line);
+			return 1;
+		}
+		bss->eap_req_id_text_len = os_strlen(bss->eap_req_id_text);
+		term = os_strstr(bss->eap_req_id_text, "\\0");
+		if (term) {
+			*term++ = '\0';
+			os_memmove(term, term + 1,
+				   bss->eap_req_id_text_len -
+				   (term - bss->eap_req_id_text) - 1);
+			bss->eap_req_id_text_len--;
+		}
+	} else if (os_strcmp(buf, "wep_key_len_broadcast") == 0) {
+		bss->default_wep_key_len = atoi(pos);
+		if (bss->default_wep_key_len > 13) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid WEP key len %lu (= %lu bits)",
+				   line,
+				   (unsigned long) bss->default_wep_key_len,
+				   (unsigned long)
+				   bss->default_wep_key_len * 8);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "wep_key_len_unicast") == 0) {
+		bss->individual_wep_key_len = atoi(pos);
+		if (bss->individual_wep_key_len < 0 ||
+		    bss->individual_wep_key_len > 13) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid WEP key len %d (= %d bits)",
+				   line, bss->individual_wep_key_len,
+				   bss->individual_wep_key_len * 8);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "wep_rekey_period") == 0) {
+		bss->wep_rekeying_period = atoi(pos);
+		if (bss->wep_rekeying_period < 0) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid period %d",
+				   line, bss->wep_rekeying_period);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "eap_reauth_period") == 0) {
+		bss->eap_reauth_period = atoi(pos);
+		if (bss->eap_reauth_period < 0) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid period %d",
+				   line, bss->eap_reauth_period);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "eapol_key_index_workaround") == 0) {
+		bss->eapol_key_index_workaround = atoi(pos);
 #ifdef CONFIG_IAPP
-		} else if (os_strcmp(buf, "iapp_interface") == 0) {
-			bss->ieee802_11f = 1;
-			os_strlcpy(bss->iapp_iface, pos,
-				   sizeof(bss->iapp_iface));
+	} else if (os_strcmp(buf, "iapp_interface") == 0) {
+		bss->ieee802_11f = 1;
+		os_strlcpy(bss->iapp_iface, pos, sizeof(bss->iapp_iface));
 #endif /* CONFIG_IAPP */
-		} else if (os_strcmp(buf, "own_ip_addr") == 0) {
-			if (hostapd_parse_ip_addr(pos, &bss->own_ip_addr)) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid IP "
-					   "address '%s'", line, pos);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "nas_identifier") == 0) {
-			bss->nas_identifier = os_strdup(pos);
+	} else if (os_strcmp(buf, "own_ip_addr") == 0) {
+		if (hostapd_parse_ip_addr(pos, &bss->own_ip_addr)) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: invalid IP address '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "nas_identifier") == 0) {
+		bss->nas_identifier = os_strdup(pos);
 #ifndef CONFIG_NO_RADIUS
-		} else if (os_strcmp(buf, "auth_server_addr") == 0) {
-			if (hostapd_config_read_radius_addr(
-				    &bss->radius->auth_servers,
-				    &bss->radius->num_auth_servers, pos, 1812,
-				    &bss->radius->auth_server)) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid IP "
-					   "address '%s'", line, pos);
-				errors++;
-			}
-		} else if (bss->radius->auth_server &&
-			   os_strcmp(buf, "auth_server_port") == 0) {
-			bss->radius->auth_server->port = atoi(pos);
-		} else if (bss->radius->auth_server &&
-			   os_strcmp(buf, "auth_server_shared_secret") == 0) {
-			int len = os_strlen(pos);
-			if (len == 0) {
-				/* RFC 2865, Ch. 3 */
-				wpa_printf(MSG_ERROR, "Line %d: empty shared "
-					   "secret is not allowed.", line);
-				errors++;
-			}
-			bss->radius->auth_server->shared_secret =
-				(u8 *) os_strdup(pos);
-			bss->radius->auth_server->shared_secret_len = len;
-		} else if (os_strcmp(buf, "acct_server_addr") == 0) {
-			if (hostapd_config_read_radius_addr(
-				    &bss->radius->acct_servers,
-				    &bss->radius->num_acct_servers, pos, 1813,
-				    &bss->radius->acct_server)) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid IP "
-					   "address '%s'", line, pos);
-				errors++;
-			}
-		} else if (bss->radius->acct_server &&
-			   os_strcmp(buf, "acct_server_port") == 0) {
-			bss->radius->acct_server->port = atoi(pos);
-		} else if (bss->radius->acct_server &&
-			   os_strcmp(buf, "acct_server_shared_secret") == 0) {
-			int len = os_strlen(pos);
-			if (len == 0) {
-				/* RFC 2865, Ch. 3 */
-				wpa_printf(MSG_ERROR, "Line %d: empty shared "
-					   "secret is not allowed.", line);
-				errors++;
-			}
-			bss->radius->acct_server->shared_secret =
-				(u8 *) os_strdup(pos);
-			bss->radius->acct_server->shared_secret_len = len;
-		} else if (os_strcmp(buf, "radius_retry_primary_interval") ==
-			   0) {
-			bss->radius->retry_primary_interval = atoi(pos);
-		} else if (os_strcmp(buf, "radius_acct_interim_interval") == 0)
-		{
-			bss->acct_interim_interval = atoi(pos);
-		} else if (os_strcmp(buf, "radius_request_cui") == 0) {
-			bss->radius_request_cui = atoi(pos);
-		} else if (os_strcmp(buf, "radius_auth_req_attr") == 0) {
-			struct hostapd_radius_attr *attr, *a;
-			attr = hostapd_parse_radius_attr(pos);
-			if (attr == NULL) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "radius_auth_req_attr", line);
-				errors++;
-			} else if (bss->radius_auth_req_attr == NULL) {
-				bss->radius_auth_req_attr = attr;
-			} else {
-				a = bss->radius_auth_req_attr;
-				while (a->next)
-					a = a->next;
-				a->next = attr;
-			}
-		} else if (os_strcmp(buf, "radius_acct_req_attr") == 0) {
-			struct hostapd_radius_attr *attr, *a;
-			attr = hostapd_parse_radius_attr(pos);
-			if (attr == NULL) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "radius_acct_req_attr", line);
-				errors++;
-			} else if (bss->radius_acct_req_attr == NULL) {
-				bss->radius_acct_req_attr = attr;
-			} else {
-				a = bss->radius_acct_req_attr;
-				while (a->next)
-					a = a->next;
-				a->next = attr;
-			}
-		} else if (os_strcmp(buf, "radius_das_port") == 0) {
-			bss->radius_das_port = atoi(pos);
-		} else if (os_strcmp(buf, "radius_das_client") == 0) {
-			if (hostapd_parse_das_client(bss, pos) < 0) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "DAS client", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "radius_das_time_window") == 0) {
-			bss->radius_das_time_window = atoi(pos);
-		} else if (os_strcmp(buf, "radius_das_require_event_timestamp")
-			   == 0) {
-			bss->radius_das_require_event_timestamp = atoi(pos);
+	} else if (os_strcmp(buf, "auth_server_addr") == 0) {
+		if (hostapd_config_read_radius_addr(
+			    &bss->radius->auth_servers,
+			    &bss->radius->num_auth_servers, pos, 1812,
+			    &bss->radius->auth_server)) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: invalid IP address '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (bss->radius->auth_server &&
+		   os_strcmp(buf, "auth_server_port") == 0) {
+		bss->radius->auth_server->port = atoi(pos);
+	} else if (bss->radius->auth_server &&
+		   os_strcmp(buf, "auth_server_shared_secret") == 0) {
+		int len = os_strlen(pos);
+		if (len == 0) {
+			/* RFC 2865, Ch. 3 */
+			wpa_printf(MSG_ERROR, "Line %d: empty shared secret is not allowed",
+				   line);
+			return 1;
+		}
+		bss->radius->auth_server->shared_secret = (u8 *) os_strdup(pos);
+		bss->radius->auth_server->shared_secret_len = len;
+	} else if (os_strcmp(buf, "acct_server_addr") == 0) {
+		if (hostapd_config_read_radius_addr(
+			    &bss->radius->acct_servers,
+			    &bss->radius->num_acct_servers, pos, 1813,
+			    &bss->radius->acct_server)) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: invalid IP address '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (bss->radius->acct_server &&
+		   os_strcmp(buf, "acct_server_port") == 0) {
+		bss->radius->acct_server->port = atoi(pos);
+	} else if (bss->radius->acct_server &&
+		   os_strcmp(buf, "acct_server_shared_secret") == 0) {
+		int len = os_strlen(pos);
+		if (len == 0) {
+			/* RFC 2865, Ch. 3 */
+			wpa_printf(MSG_ERROR, "Line %d: empty shared secret is not allowed",
+				   line);
+			return 1;
+		}
+		bss->radius->acct_server->shared_secret = (u8 *) os_strdup(pos);
+		bss->radius->acct_server->shared_secret_len = len;
+	} else if (os_strcmp(buf, "radius_retry_primary_interval") == 0) {
+		bss->radius->retry_primary_interval = atoi(pos);
+	} else if (os_strcmp(buf, "radius_acct_interim_interval") == 0) {
+		bss->acct_interim_interval = atoi(pos);
+	} else if (os_strcmp(buf, "radius_request_cui") == 0) {
+		bss->radius_request_cui = atoi(pos);
+	} else if (os_strcmp(buf, "radius_auth_req_attr") == 0) {
+		struct hostapd_radius_attr *attr, *a;
+		attr = hostapd_parse_radius_attr(pos);
+		if (attr == NULL) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: invalid radius_auth_req_attr",
+				   line);
+			return 1;
+		} else if (bss->radius_auth_req_attr == NULL) {
+			bss->radius_auth_req_attr = attr;
+		} else {
+			a = bss->radius_auth_req_attr;
+			while (a->next)
+				a = a->next;
+			a->next = attr;
+		}
+	} else if (os_strcmp(buf, "radius_acct_req_attr") == 0) {
+		struct hostapd_radius_attr *attr, *a;
+		attr = hostapd_parse_radius_attr(pos);
+		if (attr == NULL) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: invalid radius_acct_req_attr",
+				   line);
+			return 1;
+		} else if (bss->radius_acct_req_attr == NULL) {
+			bss->radius_acct_req_attr = attr;
+		} else {
+			a = bss->radius_acct_req_attr;
+			while (a->next)
+				a = a->next;
+			a->next = attr;
+		}
+	} else if (os_strcmp(buf, "radius_das_port") == 0) {
+		bss->radius_das_port = atoi(pos);
+	} else if (os_strcmp(buf, "radius_das_client") == 0) {
+		if (hostapd_parse_das_client(bss, pos) < 0) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid DAS client",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "radius_das_time_window") == 0) {
+		bss->radius_das_time_window = atoi(pos);
+	} else if (os_strcmp(buf, "radius_das_require_event_timestamp") == 0) {
+		bss->radius_das_require_event_timestamp = atoi(pos);
 #endif /* CONFIG_NO_RADIUS */
-		} else if (os_strcmp(buf, "auth_algs") == 0) {
-			bss->auth_algs = atoi(pos);
-			if (bss->auth_algs == 0) {
-				wpa_printf(MSG_ERROR, "Line %d: no "
-					   "authentication algorithms allowed",
-					   line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "max_num_sta") == 0) {
-			bss->max_num_sta = atoi(pos);
-			if (bss->max_num_sta < 0 ||
-			    bss->max_num_sta > MAX_STA_COUNT) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "max_num_sta=%d; allowed range "
-					   "0..%d", line, bss->max_num_sta,
-					   MAX_STA_COUNT);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "wpa") == 0) {
-			bss->wpa = atoi(pos);
-		} else if (os_strcmp(buf, "wpa_group_rekey") == 0) {
-			bss->wpa_group_rekey = atoi(pos);
-		} else if (os_strcmp(buf, "wpa_strict_rekey") == 0) {
-			bss->wpa_strict_rekey = atoi(pos);
-		} else if (os_strcmp(buf, "wpa_gmk_rekey") == 0) {
-			bss->wpa_gmk_rekey = atoi(pos);
-		} else if (os_strcmp(buf, "wpa_ptk_rekey") == 0) {
-			bss->wpa_ptk_rekey = atoi(pos);
-		} else if (os_strcmp(buf, "wpa_passphrase") == 0) {
-			int len = os_strlen(pos);
-			if (len < 8 || len > 63) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid WPA "
-					   "passphrase length %d (expected "
-					   "8..63)", line, len);
-				errors++;
-			} else {
-				os_free(bss->ssid.wpa_passphrase);
-				bss->ssid.wpa_passphrase = os_strdup(pos);
-				if (bss->ssid.wpa_passphrase) {
-					os_free(bss->ssid.wpa_psk);
-					bss->ssid.wpa_psk = NULL;
-					bss->ssid.wpa_passphrase_set = 1;
-				}
-			}
-		} else if (os_strcmp(buf, "wpa_psk") == 0) {
+	} else if (os_strcmp(buf, "auth_algs") == 0) {
+		bss->auth_algs = atoi(pos);
+		if (bss->auth_algs == 0) {
+			wpa_printf(MSG_ERROR, "Line %d: no authentication algorithms allowed",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "max_num_sta") == 0) {
+		bss->max_num_sta = atoi(pos);
+		if (bss->max_num_sta < 0 ||
+		    bss->max_num_sta > MAX_STA_COUNT) {
+			wpa_printf(MSG_ERROR, "Line %d: Invalid max_num_sta=%d; allowed range 0..%d",
+				   line, bss->max_num_sta, MAX_STA_COUNT);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "wpa") == 0) {
+		bss->wpa = atoi(pos);
+	} else if (os_strcmp(buf, "wpa_group_rekey") == 0) {
+		bss->wpa_group_rekey = atoi(pos);
+	} else if (os_strcmp(buf, "wpa_strict_rekey") == 0) {
+		bss->wpa_strict_rekey = atoi(pos);
+	} else if (os_strcmp(buf, "wpa_gmk_rekey") == 0) {
+		bss->wpa_gmk_rekey = atoi(pos);
+	} else if (os_strcmp(buf, "wpa_ptk_rekey") == 0) {
+		bss->wpa_ptk_rekey = atoi(pos);
+	} else if (os_strcmp(buf, "wpa_passphrase") == 0) {
+		int len = os_strlen(pos);
+		if (len < 8 || len > 63) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid WPA passphrase length %d (expected 8..63)",
+				   line, len);
+			return 1;
+		}
+		os_free(bss->ssid.wpa_passphrase);
+		bss->ssid.wpa_passphrase = os_strdup(pos);
+		if (bss->ssid.wpa_passphrase) {
 			os_free(bss->ssid.wpa_psk);
-			bss->ssid.wpa_psk =
-				os_zalloc(sizeof(struct hostapd_wpa_psk));
-			if (bss->ssid.wpa_psk == NULL)
-				errors++;
-			else if (hexstr2bin(pos, bss->ssid.wpa_psk->psk,
-					    PMK_LEN) ||
-				 pos[PMK_LEN * 2] != '\0') {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid PSK "
-					   "'%s'.", line, pos);
-				errors++;
-			} else {
-				bss->ssid.wpa_psk->group = 1;
-				os_free(bss->ssid.wpa_passphrase);
-				bss->ssid.wpa_passphrase = NULL;
-				bss->ssid.wpa_psk_set = 1;
-			}
-		} else if (os_strcmp(buf, "wpa_psk_file") == 0) {
-			os_free(bss->ssid.wpa_psk_file);
-			bss->ssid.wpa_psk_file = os_strdup(pos);
-			if (!bss->ssid.wpa_psk_file) {
-				wpa_printf(MSG_ERROR, "Line %d: allocation "
-					   "failed", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "wpa_key_mgmt") == 0) {
-			bss->wpa_key_mgmt =
-				hostapd_config_parse_key_mgmt(line, pos);
-			if (bss->wpa_key_mgmt == -1)
-				errors++;
-		} else if (os_strcmp(buf, "wpa_psk_radius") == 0) {
-			bss->wpa_psk_radius = atoi(pos);
-			if (bss->wpa_psk_radius != PSK_RADIUS_IGNORED &&
-			    bss->wpa_psk_radius != PSK_RADIUS_ACCEPTED &&
-			    bss->wpa_psk_radius != PSK_RADIUS_REQUIRED) {
-				wpa_printf(MSG_ERROR, "Line %d: unknown "
-					   "wpa_psk_radius %d",
-					   line, bss->wpa_psk_radius);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "wpa_pairwise") == 0) {
-			bss->wpa_pairwise =
-				hostapd_config_parse_cipher(line, pos);
-			if (bss->wpa_pairwise == -1 ||
-			    bss->wpa_pairwise == 0)
-				errors++;
-			else if (bss->wpa_pairwise &
-				 (WPA_CIPHER_NONE | WPA_CIPHER_WEP40 |
-				  WPA_CIPHER_WEP104)) {
-				wpa_printf(MSG_ERROR, "Line %d: unsupported "
-					   "pairwise cipher suite '%s'",
-					   bss->wpa_pairwise, pos);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "rsn_pairwise") == 0) {
-			bss->rsn_pairwise =
-				hostapd_config_parse_cipher(line, pos);
-			if (bss->rsn_pairwise == -1 ||
-			    bss->rsn_pairwise == 0)
-				errors++;
-			else if (bss->rsn_pairwise &
-				 (WPA_CIPHER_NONE | WPA_CIPHER_WEP40 |
-				  WPA_CIPHER_WEP104)) {
-				wpa_printf(MSG_ERROR, "Line %d: unsupported "
-					   "pairwise cipher suite '%s'",
-					   bss->rsn_pairwise, pos);
-				errors++;
-			}
+			bss->ssid.wpa_psk = NULL;
+			bss->ssid.wpa_passphrase_set = 1;
+		}
+	} else if (os_strcmp(buf, "wpa_psk") == 0) {
+		os_free(bss->ssid.wpa_psk);
+		bss->ssid.wpa_psk = os_zalloc(sizeof(struct hostapd_wpa_psk));
+		if (bss->ssid.wpa_psk == NULL)
+			return 1;
+		if (hexstr2bin(pos, bss->ssid.wpa_psk->psk, PMK_LEN) ||
+		    pos[PMK_LEN * 2] != '\0') {
+			wpa_printf(MSG_ERROR, "Line %d: Invalid PSK '%s'.",
+				   line, pos);
+			os_free(bss->ssid.wpa_psk);
+			bss->ssid.wpa_psk = NULL;
+			return 1;
+		}
+		bss->ssid.wpa_psk->group = 1;
+		os_free(bss->ssid.wpa_passphrase);
+		bss->ssid.wpa_passphrase = NULL;
+		bss->ssid.wpa_psk_set = 1;
+	} else if (os_strcmp(buf, "wpa_psk_file") == 0) {
+		os_free(bss->ssid.wpa_psk_file);
+		bss->ssid.wpa_psk_file = os_strdup(pos);
+		if (!bss->ssid.wpa_psk_file) {
+			wpa_printf(MSG_ERROR, "Line %d: allocation failed",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "wpa_key_mgmt") == 0) {
+		bss->wpa_key_mgmt = hostapd_config_parse_key_mgmt(line, pos);
+		if (bss->wpa_key_mgmt == -1)
+			return 1;
+	} else if (os_strcmp(buf, "wpa_psk_radius") == 0) {
+		bss->wpa_psk_radius = atoi(pos);
+		if (bss->wpa_psk_radius != PSK_RADIUS_IGNORED &&
+		    bss->wpa_psk_radius != PSK_RADIUS_ACCEPTED &&
+		    bss->wpa_psk_radius != PSK_RADIUS_REQUIRED) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: unknown wpa_psk_radius %d",
+				   line, bss->wpa_psk_radius);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "wpa_pairwise") == 0) {
+		bss->wpa_pairwise = hostapd_config_parse_cipher(line, pos);
+		if (bss->wpa_pairwise == -1 || bss->wpa_pairwise == 0)
+			return 1;
+		if (bss->wpa_pairwise &
+		    (WPA_CIPHER_NONE | WPA_CIPHER_WEP40 | WPA_CIPHER_WEP104)) {
+			wpa_printf(MSG_ERROR, "Line %d: unsupported pairwise cipher suite '%s'",
+				   bss->wpa_pairwise, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "rsn_pairwise") == 0) {
+		bss->rsn_pairwise = hostapd_config_parse_cipher(line, pos);
+		if (bss->rsn_pairwise == -1 || bss->rsn_pairwise == 0)
+			return 1;
+		if (bss->rsn_pairwise &
+		    (WPA_CIPHER_NONE | WPA_CIPHER_WEP40 | WPA_CIPHER_WEP104)) {
+			wpa_printf(MSG_ERROR, "Line %d: unsupported pairwise cipher suite '%s'",
+				   bss->rsn_pairwise, pos);
+			return 1;
+		}
 #ifdef CONFIG_RSN_PREAUTH
-		} else if (os_strcmp(buf, "rsn_preauth") == 0) {
-			bss->rsn_preauth = atoi(pos);
-		} else if (os_strcmp(buf, "rsn_preauth_interfaces") == 0) {
-			bss->rsn_preauth_interfaces = os_strdup(pos);
+	} else if (os_strcmp(buf, "rsn_preauth") == 0) {
+		bss->rsn_preauth = atoi(pos);
+	} else if (os_strcmp(buf, "rsn_preauth_interfaces") == 0) {
+		bss->rsn_preauth_interfaces = os_strdup(pos);
 #endif /* CONFIG_RSN_PREAUTH */
 #ifdef CONFIG_PEERKEY
-		} else if (os_strcmp(buf, "peerkey") == 0) {
-			bss->peerkey = atoi(pos);
+	} else if (os_strcmp(buf, "peerkey") == 0) {
+		bss->peerkey = atoi(pos);
 #endif /* CONFIG_PEERKEY */
 #ifdef CONFIG_IEEE80211R
-		} else if (os_strcmp(buf, "mobility_domain") == 0) {
-			if (os_strlen(pos) != 2 * MOBILITY_DOMAIN_ID_LEN ||
-			    hexstr2bin(pos, bss->mobility_domain,
-				       MOBILITY_DOMAIN_ID_LEN) != 0) {
-				wpa_printf(MSG_DEBUG, "Line %d: Invalid "
-					   "mobility_domain '%s'", line, pos);
-				errors++;
-				return errors;
-			}
-		} else if (os_strcmp(buf, "r1_key_holder") == 0) {
-			if (os_strlen(pos) != 2 * FT_R1KH_ID_LEN ||
-			    hexstr2bin(pos, bss->r1_key_holder,
-				       FT_R1KH_ID_LEN) != 0) {
-				wpa_printf(MSG_DEBUG, "Line %d: Invalid "
-					   "r1_key_holder '%s'", line, pos);
-				errors++;
-				return errors;
-			}
-		} else if (os_strcmp(buf, "r0_key_lifetime") == 0) {
-			bss->r0_key_lifetime = atoi(pos);
-		} else if (os_strcmp(buf, "reassociation_deadline") == 0) {
-			bss->reassociation_deadline = atoi(pos);
-		} else if (os_strcmp(buf, "r0kh") == 0) {
-			if (add_r0kh(bss, pos) < 0) {
-				wpa_printf(MSG_DEBUG, "Line %d: Invalid "
-					   "r0kh '%s'", line, pos);
-				errors++;
-				return errors;
-			}
-		} else if (os_strcmp(buf, "r1kh") == 0) {
-			if (add_r1kh(bss, pos) < 0) {
-				wpa_printf(MSG_DEBUG, "Line %d: Invalid "
-					   "r1kh '%s'", line, pos);
-				errors++;
-				return errors;
-			}
-		} else if (os_strcmp(buf, "pmk_r1_push") == 0) {
-			bss->pmk_r1_push = atoi(pos);
-		} else if (os_strcmp(buf, "ft_over_ds") == 0) {
-			bss->ft_over_ds = atoi(pos);
+	} else if (os_strcmp(buf, "mobility_domain") == 0) {
+		if (os_strlen(pos) != 2 * MOBILITY_DOMAIN_ID_LEN ||
+		    hexstr2bin(pos, bss->mobility_domain,
+			       MOBILITY_DOMAIN_ID_LEN) != 0) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: Invalid mobility_domain '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "r1_key_holder") == 0) {
+		if (os_strlen(pos) != 2 * FT_R1KH_ID_LEN ||
+		    hexstr2bin(pos, bss->r1_key_holder, FT_R1KH_ID_LEN) != 0) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: Invalid r1_key_holder '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "r0_key_lifetime") == 0) {
+		bss->r0_key_lifetime = atoi(pos);
+	} else if (os_strcmp(buf, "reassociation_deadline") == 0) {
+		bss->reassociation_deadline = atoi(pos);
+	} else if (os_strcmp(buf, "r0kh") == 0) {
+		if (add_r0kh(bss, pos) < 0) {
+			wpa_printf(MSG_DEBUG, "Line %d: Invalid r0kh '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "r1kh") == 0) {
+		if (add_r1kh(bss, pos) < 0) {
+			wpa_printf(MSG_DEBUG, "Line %d: Invalid r1kh '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "pmk_r1_push") == 0) {
+		bss->pmk_r1_push = atoi(pos);
+	} else if (os_strcmp(buf, "ft_over_ds") == 0) {
+		bss->ft_over_ds = atoi(pos);
 #endif /* CONFIG_IEEE80211R */
 #ifndef CONFIG_NO_CTRL_IFACE
-		} else if (os_strcmp(buf, "ctrl_interface") == 0) {
-			os_free(bss->ctrl_interface);
-			bss->ctrl_interface = os_strdup(pos);
-		} else if (os_strcmp(buf, "ctrl_interface_group") == 0) {
+	} else if (os_strcmp(buf, "ctrl_interface") == 0) {
+		os_free(bss->ctrl_interface);
+		bss->ctrl_interface = os_strdup(pos);
+	} else if (os_strcmp(buf, "ctrl_interface_group") == 0) {
 #ifndef CONFIG_NATIVE_WINDOWS
-			struct group *grp;
-			char *endp;
-			const char *group = pos;
+		struct group *grp;
+		char *endp;
+		const char *group = pos;
 
-			grp = getgrnam(group);
-			if (grp) {
-				bss->ctrl_interface_gid = grp->gr_gid;
-				bss->ctrl_interface_gid_set = 1;
-				wpa_printf(MSG_DEBUG, "ctrl_interface_group=%d"
-					   " (from group name '%s')",
-					   bss->ctrl_interface_gid, group);
-				return errors;
-			}
-
-			/* Group name not found - try to parse this as gid */
-			bss->ctrl_interface_gid = strtol(group, &endp, 10);
-			if (*group == '\0' || *endp != '\0') {
-				wpa_printf(MSG_DEBUG, "Line %d: Invalid group "
-					   "'%s'", line, group);
-				errors++;
-				return errors;
-			}
+		grp = getgrnam(group);
+		if (grp) {
+			bss->ctrl_interface_gid = grp->gr_gid;
 			bss->ctrl_interface_gid_set = 1;
-			wpa_printf(MSG_DEBUG, "ctrl_interface_group=%d",
-				   bss->ctrl_interface_gid);
+			wpa_printf(MSG_DEBUG, "ctrl_interface_group=%d (from group name '%s')",
+				   bss->ctrl_interface_gid, group);
+			return 0;
+		}
+
+		/* Group name not found - try to parse this as gid */
+		bss->ctrl_interface_gid = strtol(group, &endp, 10);
+		if (*group == '\0' || *endp != '\0') {
+			wpa_printf(MSG_DEBUG, "Line %d: Invalid group '%s'",
+				   line, group);
+			return 1;
+		}
+		bss->ctrl_interface_gid_set = 1;
+		wpa_printf(MSG_DEBUG, "ctrl_interface_group=%d",
+			   bss->ctrl_interface_gid);
 #endif /* CONFIG_NATIVE_WINDOWS */
 #endif /* CONFIG_NO_CTRL_IFACE */
 #ifdef RADIUS_SERVER
-		} else if (os_strcmp(buf, "radius_server_clients") == 0) {
-			os_free(bss->radius_server_clients);
-			bss->radius_server_clients = os_strdup(pos);
-		} else if (os_strcmp(buf, "radius_server_auth_port") == 0) {
-			bss->radius_server_auth_port = atoi(pos);
-		} else if (os_strcmp(buf, "radius_server_acct_port") == 0) {
-			bss->radius_server_acct_port = atoi(pos);
-		} else if (os_strcmp(buf, "radius_server_ipv6") == 0) {
-			bss->radius_server_ipv6 = atoi(pos);
+	} else if (os_strcmp(buf, "radius_server_clients") == 0) {
+		os_free(bss->radius_server_clients);
+		bss->radius_server_clients = os_strdup(pos);
+	} else if (os_strcmp(buf, "radius_server_auth_port") == 0) {
+		bss->radius_server_auth_port = atoi(pos);
+	} else if (os_strcmp(buf, "radius_server_acct_port") == 0) {
+		bss->radius_server_acct_port = atoi(pos);
+	} else if (os_strcmp(buf, "radius_server_ipv6") == 0) {
+		bss->radius_server_ipv6 = atoi(pos);
 #endif /* RADIUS_SERVER */
-		} else if (os_strcmp(buf, "test_socket") == 0) {
-			os_free(bss->test_socket);
-			bss->test_socket = os_strdup(pos);
-		} else if (os_strcmp(buf, "use_pae_group_addr") == 0) {
-			bss->use_pae_group_addr = atoi(pos);
-		} else if (os_strcmp(buf, "hw_mode") == 0) {
-			if (os_strcmp(pos, "a") == 0)
-				conf->hw_mode = HOSTAPD_MODE_IEEE80211A;
-			else if (os_strcmp(pos, "b") == 0)
-				conf->hw_mode = HOSTAPD_MODE_IEEE80211B;
-			else if (os_strcmp(pos, "g") == 0)
-				conf->hw_mode = HOSTAPD_MODE_IEEE80211G;
-			else if (os_strcmp(pos, "ad") == 0)
-				conf->hw_mode = HOSTAPD_MODE_IEEE80211AD;
-			else {
-				wpa_printf(MSG_ERROR, "Line %d: unknown "
-					   "hw_mode '%s'", line, pos);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "wps_rf_bands") == 0) {
-			if (os_strcmp(pos, "a") == 0)
-				bss->wps_rf_bands = WPS_RF_50GHZ;
-			else if (os_strcmp(pos, "g") == 0 ||
-				 os_strcmp(pos, "b") == 0)
-				bss->wps_rf_bands = WPS_RF_24GHZ;
-			else if (os_strcmp(pos, "ag") == 0 ||
-				 os_strcmp(pos, "ga") == 0)
-				bss->wps_rf_bands =
-					WPS_RF_24GHZ | WPS_RF_50GHZ;
-			else {
-				wpa_printf(MSG_ERROR, "Line %d: unknown "
-					   "wps_rf_band '%s'", line, pos);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "channel") == 0) {
-			if (os_strcmp(pos, "acs_survey") == 0) {
+	} else if (os_strcmp(buf, "test_socket") == 0) {
+		os_free(bss->test_socket);
+		bss->test_socket = os_strdup(pos);
+	} else if (os_strcmp(buf, "use_pae_group_addr") == 0) {
+		bss->use_pae_group_addr = atoi(pos);
+	} else if (os_strcmp(buf, "hw_mode") == 0) {
+		if (os_strcmp(pos, "a") == 0)
+			conf->hw_mode = HOSTAPD_MODE_IEEE80211A;
+		else if (os_strcmp(pos, "b") == 0)
+			conf->hw_mode = HOSTAPD_MODE_IEEE80211B;
+		else if (os_strcmp(pos, "g") == 0)
+			conf->hw_mode = HOSTAPD_MODE_IEEE80211G;
+		else if (os_strcmp(pos, "ad") == 0)
+			conf->hw_mode = HOSTAPD_MODE_IEEE80211AD;
+		else {
+			wpa_printf(MSG_ERROR, "Line %d: unknown hw_mode '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "wps_rf_bands") == 0) {
+		if (os_strcmp(pos, "a") == 0)
+			bss->wps_rf_bands = WPS_RF_50GHZ;
+		else if (os_strcmp(pos, "g") == 0 ||
+			 os_strcmp(pos, "b") == 0)
+			bss->wps_rf_bands = WPS_RF_24GHZ;
+		else if (os_strcmp(pos, "ag") == 0 ||
+			 os_strcmp(pos, "ga") == 0)
+			bss->wps_rf_bands = WPS_RF_24GHZ | WPS_RF_50GHZ;
+		else {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: unknown wps_rf_band '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "channel") == 0) {
+		if (os_strcmp(pos, "acs_survey") == 0) {
 #ifndef CONFIG_ACS
-				wpa_printf(MSG_ERROR, "Line %d: tries to enable ACS but CONFIG_ACS disabled",
-					   line);
-				errors++;
+			wpa_printf(MSG_ERROR, "Line %d: tries to enable ACS but CONFIG_ACS disabled",
+				   line);
+			return 1;
 #endif /* CONFIG_ACS */
-				conf->channel = 0;
-			} else
-				conf->channel = atoi(pos);
-		} else if (os_strcmp(buf, "chanlist") == 0) {
-			if (hostapd_parse_intlist(&conf->chanlist, pos)) {
-				wpa_printf(MSG_ERROR,
-					   "Line %d: invalid channel list",
-					   line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "beacon_int") == 0) {
-			int val = atoi(pos);
-			/* MIB defines range as 1..65535, but very small values
-			 * cause problems with the current implementation.
-			 * Since it is unlikely that this small numbers are
-			 * useful in real life scenarios, do not allow beacon
-			 * period to be set below 15 TU. */
-			if (val < 15 || val > 65535) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "beacon_int %d (expected "
-					   "15..65535)", line, val);
-				errors++;
-			} else
-				conf->beacon_int = val;
+			conf->channel = 0;
+		} else
+			conf->channel = atoi(pos);
+	} else if (os_strcmp(buf, "chanlist") == 0) {
+		if (hostapd_parse_intlist(&conf->chanlist, pos)) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid channel list",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "beacon_int") == 0) {
+		int val = atoi(pos);
+		/* MIB defines range as 1..65535, but very small values
+		 * cause problems with the current implementation.
+		 * Since it is unlikely that this small numbers are
+		 * useful in real life scenarios, do not allow beacon
+		 * period to be set below 15 TU. */
+		if (val < 15 || val > 65535) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid beacon_int %d (expected 15..65535)",
+				   line, val);
+			return 1;
+		}
+		conf->beacon_int = val;
 #ifdef CONFIG_ACS
-		} else if (os_strcmp(buf, "acs_num_scans") == 0) {
-			int val = atoi(pos);
-			if (val <= 0 || val > 100) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid acs_num_scans %d (expected 1..100)",
-					   line, val);
-				errors++;
-			} else
-				conf->acs_num_scans = val;
+	} else if (os_strcmp(buf, "acs_num_scans") == 0) {
+		int val = atoi(pos);
+		if (val <= 0 || val > 100) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid acs_num_scans %d (expected 1..100)",
+				   line, val);
+			return 1;
+		}
+		conf->acs_num_scans = val;
 #endif /* CONFIG_ACS */
-		} else if (os_strcmp(buf, "dtim_period") == 0) {
-			bss->dtim_period = atoi(pos);
-			if (bss->dtim_period < 1 || bss->dtim_period > 255) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "dtim_period %d",
-					   line, bss->dtim_period);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "rts_threshold") == 0) {
-			conf->rts_threshold = atoi(pos);
-			if (conf->rts_threshold < 0 ||
-			    conf->rts_threshold > 2347) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "rts_threshold %d",
-					   line, conf->rts_threshold);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "fragm_threshold") == 0) {
-			conf->fragm_threshold = atoi(pos);
-			if (conf->fragm_threshold < 256 ||
-			    conf->fragm_threshold > 2346) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "fragm_threshold %d",
-					   line, conf->fragm_threshold);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "send_probe_response") == 0) {
-			int val = atoi(pos);
-			if (val != 0 && val != 1) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "send_probe_response %d (expected "
-					   "0 or 1)", line, val);
-			} else
-				conf->send_probe_response = val;
-		} else if (os_strcmp(buf, "supported_rates") == 0) {
-			if (hostapd_parse_intlist(&conf->supported_rates, pos))
-			{
-				wpa_printf(MSG_ERROR, "Line %d: invalid rate "
-					   "list", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "basic_rates") == 0) {
-			if (hostapd_parse_intlist(&conf->basic_rates, pos)) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid rate "
-					   "list", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "preamble") == 0) {
-			if (atoi(pos))
-				conf->preamble = SHORT_PREAMBLE;
-			else
-				conf->preamble = LONG_PREAMBLE;
-		} else if (os_strcmp(buf, "ignore_broadcast_ssid") == 0) {
-			bss->ignore_broadcast_ssid = atoi(pos);
-		} else if (os_strcmp(buf, "wep_default_key") == 0) {
-			bss->ssid.wep.idx = atoi(pos);
-			if (bss->ssid.wep.idx > 3) {
-				wpa_printf(MSG_ERROR, "Invalid "
-					   "wep_default_key index %d",
-					   bss->ssid.wep.idx);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "wep_key0") == 0 ||
-			   os_strcmp(buf, "wep_key1") == 0 ||
-			   os_strcmp(buf, "wep_key2") == 0 ||
-			   os_strcmp(buf, "wep_key3") == 0) {
-			if (hostapd_config_read_wep(&bss->ssid.wep,
-						    buf[7] - '0', pos)) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid WEP "
-					   "key '%s'", line, buf);
-				errors++;
-			}
+	} else if (os_strcmp(buf, "dtim_period") == 0) {
+		bss->dtim_period = atoi(pos);
+		if (bss->dtim_period < 1 || bss->dtim_period > 255) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid dtim_period %d",
+				   line, bss->dtim_period);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "rts_threshold") == 0) {
+		conf->rts_threshold = atoi(pos);
+		if (conf->rts_threshold < 0 || conf->rts_threshold > 2347) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: invalid rts_threshold %d",
+				   line, conf->rts_threshold);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "fragm_threshold") == 0) {
+		conf->fragm_threshold = atoi(pos);
+		if (conf->fragm_threshold < 256 ||
+		    conf->fragm_threshold > 2346) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: invalid fragm_threshold %d",
+				   line, conf->fragm_threshold);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "send_probe_response") == 0) {
+		int val = atoi(pos);
+		if (val != 0 && val != 1) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid send_probe_response %d (expected 0 or 1)",
+				   line, val);
+			return 1;
+		}
+		conf->send_probe_response = val;
+	} else if (os_strcmp(buf, "supported_rates") == 0) {
+		if (hostapd_parse_intlist(&conf->supported_rates, pos)) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid rate list",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "basic_rates") == 0) {
+		if (hostapd_parse_intlist(&conf->basic_rates, pos)) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid rate list",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "preamble") == 0) {
+		if (atoi(pos))
+			conf->preamble = SHORT_PREAMBLE;
+		else
+			conf->preamble = LONG_PREAMBLE;
+	} else if (os_strcmp(buf, "ignore_broadcast_ssid") == 0) {
+		bss->ignore_broadcast_ssid = atoi(pos);
+	} else if (os_strcmp(buf, "wep_default_key") == 0) {
+		bss->ssid.wep.idx = atoi(pos);
+		if (bss->ssid.wep.idx > 3) {
+			wpa_printf(MSG_ERROR,
+				   "Invalid wep_default_key index %d",
+				   bss->ssid.wep.idx);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "wep_key0") == 0 ||
+		   os_strcmp(buf, "wep_key1") == 0 ||
+		   os_strcmp(buf, "wep_key2") == 0 ||
+		   os_strcmp(buf, "wep_key3") == 0) {
+		if (hostapd_config_read_wep(&bss->ssid.wep,
+					    buf[7] - '0', pos)) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid WEP key '%s'",
+				   line, buf);
+			return 1;
+		}
 #ifndef CONFIG_NO_VLAN
-		} else if (os_strcmp(buf, "dynamic_vlan") == 0) {
-			bss->ssid.dynamic_vlan = atoi(pos);
-		} else if (os_strcmp(buf, "vlan_file") == 0) {
-			if (hostapd_config_read_vlan_file(bss, pos)) {
-				wpa_printf(MSG_ERROR, "Line %d: failed to "
-					   "read VLAN file '%s'", line, pos);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "vlan_naming") == 0) {
-			bss->ssid.vlan_naming = atoi(pos);
-			if (bss->ssid.vlan_naming >= DYNAMIC_VLAN_NAMING_END ||
-			    bss->ssid.vlan_naming < 0) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "naming scheme %d", line,
-                                           bss->ssid.vlan_naming);
-				errors++;
-                        }
+	} else if (os_strcmp(buf, "dynamic_vlan") == 0) {
+		bss->ssid.dynamic_vlan = atoi(pos);
+	} else if (os_strcmp(buf, "vlan_file") == 0) {
+		if (hostapd_config_read_vlan_file(bss, pos)) {
+			wpa_printf(MSG_ERROR, "Line %d: failed to read VLAN file '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "vlan_naming") == 0) {
+		bss->ssid.vlan_naming = atoi(pos);
+		if (bss->ssid.vlan_naming >= DYNAMIC_VLAN_NAMING_END ||
+		    bss->ssid.vlan_naming < 0) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: invalid naming scheme %d",
+				   line, bss->ssid.vlan_naming);
+			return 1;
+		}
 #ifdef CONFIG_FULL_DYNAMIC_VLAN
-		} else if (os_strcmp(buf, "vlan_tagged_interface") == 0) {
-			bss->ssid.vlan_tagged_interface = os_strdup(pos);
+	} else if (os_strcmp(buf, "vlan_tagged_interface") == 0) {
+		bss->ssid.vlan_tagged_interface = os_strdup(pos);
 #endif /* CONFIG_FULL_DYNAMIC_VLAN */
 #endif /* CONFIG_NO_VLAN */
-		} else if (os_strcmp(buf, "ap_table_max_size") == 0) {
-			conf->ap_table_max_size = atoi(pos);
-		} else if (os_strcmp(buf, "ap_table_expiration_time") == 0) {
-			conf->ap_table_expiration_time = atoi(pos);
-		} else if (os_strncmp(buf, "tx_queue_", 9) == 0) {
-			if (hostapd_config_tx_queue(conf, buf, pos)) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid TX "
-					   "queue item", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "wme_enabled") == 0 ||
-			   os_strcmp(buf, "wmm_enabled") == 0) {
-			bss->wmm_enabled = atoi(pos);
-		} else if (os_strcmp(buf, "uapsd_advertisement_enabled") == 0) {
-			bss->wmm_uapsd = atoi(pos);
-		} else if (os_strncmp(buf, "wme_ac_", 7) == 0 ||
-			   os_strncmp(buf, "wmm_ac_", 7) == 0) {
-			if (hostapd_config_wmm_ac(conf->wmm_ac_params, buf,
-						  pos)) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid WMM "
-					   "ac item", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "bss") == 0) {
-			if (hostapd_config_bss(conf, pos)) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid bss "
-					   "item", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "bssid") == 0) {
-			if (hwaddr_aton(pos, bss->bssid)) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid bssid "
-					   "item", line);
-				errors++;
-			}
+	} else if (os_strcmp(buf, "ap_table_max_size") == 0) {
+		conf->ap_table_max_size = atoi(pos);
+	} else if (os_strcmp(buf, "ap_table_expiration_time") == 0) {
+		conf->ap_table_expiration_time = atoi(pos);
+	} else if (os_strncmp(buf, "tx_queue_", 9) == 0) {
+		if (hostapd_config_tx_queue(conf, buf, pos)) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid TX queue item",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "wme_enabled") == 0 ||
+		   os_strcmp(buf, "wmm_enabled") == 0) {
+		bss->wmm_enabled = atoi(pos);
+	} else if (os_strcmp(buf, "uapsd_advertisement_enabled") == 0) {
+		bss->wmm_uapsd = atoi(pos);
+	} else if (os_strncmp(buf, "wme_ac_", 7) == 0 ||
+		   os_strncmp(buf, "wmm_ac_", 7) == 0) {
+		if (hostapd_config_wmm_ac(conf->wmm_ac_params, buf, pos)) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid WMM ac item",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "bss") == 0) {
+		if (hostapd_config_bss(conf, pos)) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid bss item",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "bssid") == 0) {
+		if (hwaddr_aton(pos, bss->bssid)) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid bssid item",
+				   line);
+			return 1;
+		}
 #ifdef CONFIG_IEEE80211W
-		} else if (os_strcmp(buf, "ieee80211w") == 0) {
-			bss->ieee80211w = atoi(pos);
-		} else if (os_strcmp(buf, "assoc_sa_query_max_timeout") == 0) {
-			bss->assoc_sa_query_max_timeout = atoi(pos);
-			if (bss->assoc_sa_query_max_timeout == 0) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "assoc_sa_query_max_timeout", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "assoc_sa_query_retry_timeout") == 0)
-		{
-			bss->assoc_sa_query_retry_timeout = atoi(pos);
-			if (bss->assoc_sa_query_retry_timeout == 0) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "assoc_sa_query_retry_timeout",
-					   line);
-				errors++;
-			}
+	} else if (os_strcmp(buf, "ieee80211w") == 0) {
+		bss->ieee80211w = atoi(pos);
+	} else if (os_strcmp(buf, "assoc_sa_query_max_timeout") == 0) {
+		bss->assoc_sa_query_max_timeout = atoi(pos);
+		if (bss->assoc_sa_query_max_timeout == 0) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid assoc_sa_query_max_timeout",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "assoc_sa_query_retry_timeout") == 0) {
+		bss->assoc_sa_query_retry_timeout = atoi(pos);
+		if (bss->assoc_sa_query_retry_timeout == 0) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid assoc_sa_query_retry_timeout",
+				   line);
+			return 1;
+		}
 #endif /* CONFIG_IEEE80211W */
 #ifdef CONFIG_IEEE80211N
-		} else if (os_strcmp(buf, "ieee80211n") == 0) {
-			conf->ieee80211n = atoi(pos);
-		} else if (os_strcmp(buf, "ht_capab") == 0) {
-			if (hostapd_config_ht_capab(conf, pos) < 0) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "ht_capab", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "require_ht") == 0) {
-			conf->require_ht = atoi(pos);
-		} else if (os_strcmp(buf, "obss_interval") == 0) {
-			conf->obss_interval = atoi(pos);
+	} else if (os_strcmp(buf, "ieee80211n") == 0) {
+		conf->ieee80211n = atoi(pos);
+	} else if (os_strcmp(buf, "ht_capab") == 0) {
+		if (hostapd_config_ht_capab(conf, pos) < 0) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid ht_capab",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "require_ht") == 0) {
+		conf->require_ht = atoi(pos);
+	} else if (os_strcmp(buf, "obss_interval") == 0) {
+		conf->obss_interval = atoi(pos);
 #endif /* CONFIG_IEEE80211N */
 #ifdef CONFIG_IEEE80211AC
-		} else if (os_strcmp(buf, "ieee80211ac") == 0) {
-			conf->ieee80211ac = atoi(pos);
-		} else if (os_strcmp(buf, "vht_capab") == 0) {
-			if (hostapd_config_vht_capab(conf, pos) < 0) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "vht_capab", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "require_vht") == 0) {
-			conf->require_vht = atoi(pos);
-		} else if (os_strcmp(buf, "vht_oper_chwidth") == 0) {
-			conf->vht_oper_chwidth = atoi(pos);
-		} else if (os_strcmp(buf, "vht_oper_centr_freq_seg0_idx") == 0)
-		{
-			conf->vht_oper_centr_freq_seg0_idx = atoi(pos);
-		} else if (os_strcmp(buf, "vht_oper_centr_freq_seg1_idx") == 0)
-		{
-			conf->vht_oper_centr_freq_seg1_idx = atoi(pos);
+	} else if (os_strcmp(buf, "ieee80211ac") == 0) {
+		conf->ieee80211ac = atoi(pos);
+	} else if (os_strcmp(buf, "vht_capab") == 0) {
+		if (hostapd_config_vht_capab(conf, pos) < 0) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid vht_capab",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "require_vht") == 0) {
+		conf->require_vht = atoi(pos);
+	} else if (os_strcmp(buf, "vht_oper_chwidth") == 0) {
+		conf->vht_oper_chwidth = atoi(pos);
+	} else if (os_strcmp(buf, "vht_oper_centr_freq_seg0_idx") == 0) {
+		conf->vht_oper_centr_freq_seg0_idx = atoi(pos);
+	} else if (os_strcmp(buf, "vht_oper_centr_freq_seg1_idx") == 0) {
+		conf->vht_oper_centr_freq_seg1_idx = atoi(pos);
 #endif /* CONFIG_IEEE80211AC */
-		} else if (os_strcmp(buf, "max_listen_interval") == 0) {
-			bss->max_listen_interval = atoi(pos);
-		} else if (os_strcmp(buf, "disable_pmksa_caching") == 0) {
-			bss->disable_pmksa_caching = atoi(pos);
-		} else if (os_strcmp(buf, "okc") == 0) {
-			bss->okc = atoi(pos);
+	} else if (os_strcmp(buf, "max_listen_interval") == 0) {
+		bss->max_listen_interval = atoi(pos);
+	} else if (os_strcmp(buf, "disable_pmksa_caching") == 0) {
+		bss->disable_pmksa_caching = atoi(pos);
+	} else if (os_strcmp(buf, "okc") == 0) {
+		bss->okc = atoi(pos);
 #ifdef CONFIG_WPS
-		} else if (os_strcmp(buf, "wps_state") == 0) {
-			bss->wps_state = atoi(pos);
-			if (bss->wps_state < 0 || bss->wps_state > 2) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "wps_state", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "wps_independent") == 0) {
-			bss->wps_independent = atoi(pos);
-		} else if (os_strcmp(buf, "ap_setup_locked") == 0) {
-			bss->ap_setup_locked = atoi(pos);
-		} else if (os_strcmp(buf, "uuid") == 0) {
-			if (uuid_str2bin(pos, bss->uuid)) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid UUID",
-					   line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "wps_pin_requests") == 0) {
-			os_free(bss->wps_pin_requests);
-			bss->wps_pin_requests = os_strdup(pos);
-		} else if (os_strcmp(buf, "device_name") == 0) {
-			if (os_strlen(pos) > 32) {
-				wpa_printf(MSG_ERROR, "Line %d: Too long "
-					   "device_name", line);
-				errors++;
-			}
-			os_free(bss->device_name);
-			bss->device_name = os_strdup(pos);
-		} else if (os_strcmp(buf, "manufacturer") == 0) {
-			if (os_strlen(pos) > 64) {
-				wpa_printf(MSG_ERROR, "Line %d: Too long "
-					   "manufacturer", line);
-				errors++;
-			}
-			os_free(bss->manufacturer);
-			bss->manufacturer = os_strdup(pos);
-		} else if (os_strcmp(buf, "model_name") == 0) {
-			if (os_strlen(pos) > 32) {
-				wpa_printf(MSG_ERROR, "Line %d: Too long "
-					   "model_name", line);
-				errors++;
-			}
-			os_free(bss->model_name);
-			bss->model_name = os_strdup(pos);
-		} else if (os_strcmp(buf, "model_number") == 0) {
-			if (os_strlen(pos) > 32) {
-				wpa_printf(MSG_ERROR, "Line %d: Too long "
-					   "model_number", line);
-				errors++;
-			}
-			os_free(bss->model_number);
-			bss->model_number = os_strdup(pos);
-		} else if (os_strcmp(buf, "serial_number") == 0) {
-			if (os_strlen(pos) > 32) {
-				wpa_printf(MSG_ERROR, "Line %d: Too long "
-					   "serial_number", line);
-				errors++;
-			}
-			os_free(bss->serial_number);
-			bss->serial_number = os_strdup(pos);
-		} else if (os_strcmp(buf, "device_type") == 0) {
-			if (wps_dev_type_str2bin(pos, bss->device_type))
-				errors++;
-		} else if (os_strcmp(buf, "config_methods") == 0) {
-			os_free(bss->config_methods);
-			bss->config_methods = os_strdup(pos);
-		} else if (os_strcmp(buf, "os_version") == 0) {
-			if (hexstr2bin(pos, bss->os_version, 4)) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "os_version", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "ap_pin") == 0) {
-			os_free(bss->ap_pin);
-			bss->ap_pin = os_strdup(pos);
-		} else if (os_strcmp(buf, "skip_cred_build") == 0) {
-			bss->skip_cred_build = atoi(pos);
-		} else if (os_strcmp(buf, "extra_cred") == 0) {
-			os_free(bss->extra_cred);
-			bss->extra_cred =
-				(u8 *) os_readfile(pos, &bss->extra_cred_len);
-			if (bss->extra_cred == NULL) {
-				wpa_printf(MSG_ERROR, "Line %d: could not "
-					   "read Credentials from '%s'",
-					   line, pos);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "wps_cred_processing") == 0) {
-			bss->wps_cred_processing = atoi(pos);
-		} else if (os_strcmp(buf, "ap_settings") == 0) {
-			os_free(bss->ap_settings);
-			bss->ap_settings =
-				(u8 *) os_readfile(pos, &bss->ap_settings_len);
-			if (bss->ap_settings == NULL) {
-				wpa_printf(MSG_ERROR, "Line %d: could not "
-					   "read AP Settings from '%s'",
-					   line, pos);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "upnp_iface") == 0) {
-			bss->upnp_iface = os_strdup(pos);
-		} else if (os_strcmp(buf, "friendly_name") == 0) {
-			os_free(bss->friendly_name);
-			bss->friendly_name = os_strdup(pos);
-		} else if (os_strcmp(buf, "manufacturer_url") == 0) {
-			os_free(bss->manufacturer_url);
-			bss->manufacturer_url = os_strdup(pos);
-		} else if (os_strcmp(buf, "model_description") == 0) {
-			os_free(bss->model_description);
-			bss->model_description = os_strdup(pos);
-		} else if (os_strcmp(buf, "model_url") == 0) {
-			os_free(bss->model_url);
-			bss->model_url = os_strdup(pos);
-		} else if (os_strcmp(buf, "upc") == 0) {
-			os_free(bss->upc);
-			bss->upc = os_strdup(pos);
-		} else if (os_strcmp(buf, "pbc_in_m1") == 0) {
-			bss->pbc_in_m1 = atoi(pos);
-		} else if (os_strcmp(buf, "server_id") == 0) {
-			os_free(bss->server_id);
-			bss->server_id = os_strdup(pos);
+	} else if (os_strcmp(buf, "wps_state") == 0) {
+		bss->wps_state = atoi(pos);
+		if (bss->wps_state < 0 || bss->wps_state > 2) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid wps_state",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "wps_independent") == 0) {
+		bss->wps_independent = atoi(pos);
+	} else if (os_strcmp(buf, "ap_setup_locked") == 0) {
+		bss->ap_setup_locked = atoi(pos);
+	} else if (os_strcmp(buf, "uuid") == 0) {
+		if (uuid_str2bin(pos, bss->uuid)) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid UUID", line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "wps_pin_requests") == 0) {
+		os_free(bss->wps_pin_requests);
+		bss->wps_pin_requests = os_strdup(pos);
+	} else if (os_strcmp(buf, "device_name") == 0) {
+		if (os_strlen(pos) > 32) {
+			wpa_printf(MSG_ERROR, "Line %d: Too long "
+				   "device_name", line);
+			return 1;
+		}
+		os_free(bss->device_name);
+		bss->device_name = os_strdup(pos);
+	} else if (os_strcmp(buf, "manufacturer") == 0) {
+		if (os_strlen(pos) > 64) {
+			wpa_printf(MSG_ERROR, "Line %d: Too long manufacturer",
+				   line);
+			return 1;
+		}
+		os_free(bss->manufacturer);
+		bss->manufacturer = os_strdup(pos);
+	} else if (os_strcmp(buf, "model_name") == 0) {
+		if (os_strlen(pos) > 32) {
+			wpa_printf(MSG_ERROR, "Line %d: Too long model_name",
+				   line);
+			return 1;
+		}
+		os_free(bss->model_name);
+		bss->model_name = os_strdup(pos);
+	} else if (os_strcmp(buf, "model_number") == 0) {
+		if (os_strlen(pos) > 32) {
+			wpa_printf(MSG_ERROR, "Line %d: Too long model_number",
+				   line);
+			return 1;
+		}
+		os_free(bss->model_number);
+		bss->model_number = os_strdup(pos);
+	} else if (os_strcmp(buf, "serial_number") == 0) {
+		if (os_strlen(pos) > 32) {
+			wpa_printf(MSG_ERROR, "Line %d: Too long serial_number",
+				   line);
+			return 1;
+		}
+		os_free(bss->serial_number);
+		bss->serial_number = os_strdup(pos);
+	} else if (os_strcmp(buf, "device_type") == 0) {
+		if (wps_dev_type_str2bin(pos, bss->device_type))
+			return 1;
+	} else if (os_strcmp(buf, "config_methods") == 0) {
+		os_free(bss->config_methods);
+		bss->config_methods = os_strdup(pos);
+	} else if (os_strcmp(buf, "os_version") == 0) {
+		if (hexstr2bin(pos, bss->os_version, 4)) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid os_version",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "ap_pin") == 0) {
+		os_free(bss->ap_pin);
+		bss->ap_pin = os_strdup(pos);
+	} else if (os_strcmp(buf, "skip_cred_build") == 0) {
+		bss->skip_cred_build = atoi(pos);
+	} else if (os_strcmp(buf, "extra_cred") == 0) {
+		os_free(bss->extra_cred);
+		bss->extra_cred = (u8 *) os_readfile(pos, &bss->extra_cred_len);
+		if (bss->extra_cred == NULL) {
+			wpa_printf(MSG_ERROR, "Line %d: could not read Credentials from '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "wps_cred_processing") == 0) {
+		bss->wps_cred_processing = atoi(pos);
+	} else if (os_strcmp(buf, "ap_settings") == 0) {
+		os_free(bss->ap_settings);
+		bss->ap_settings =
+			(u8 *) os_readfile(pos, &bss->ap_settings_len);
+		if (bss->ap_settings == NULL) {
+			wpa_printf(MSG_ERROR, "Line %d: could not read AP Settings from '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "upnp_iface") == 0) {
+		bss->upnp_iface = os_strdup(pos);
+	} else if (os_strcmp(buf, "friendly_name") == 0) {
+		os_free(bss->friendly_name);
+		bss->friendly_name = os_strdup(pos);
+	} else if (os_strcmp(buf, "manufacturer_url") == 0) {
+		os_free(bss->manufacturer_url);
+		bss->manufacturer_url = os_strdup(pos);
+	} else if (os_strcmp(buf, "model_description") == 0) {
+		os_free(bss->model_description);
+		bss->model_description = os_strdup(pos);
+	} else if (os_strcmp(buf, "model_url") == 0) {
+		os_free(bss->model_url);
+		bss->model_url = os_strdup(pos);
+	} else if (os_strcmp(buf, "upc") == 0) {
+		os_free(bss->upc);
+		bss->upc = os_strdup(pos);
+	} else if (os_strcmp(buf, "pbc_in_m1") == 0) {
+		bss->pbc_in_m1 = atoi(pos);
+	} else if (os_strcmp(buf, "server_id") == 0) {
+		os_free(bss->server_id);
+		bss->server_id = os_strdup(pos);
 #ifdef CONFIG_WPS_NFC
-		} else if (os_strcmp(buf, "wps_nfc_dev_pw_id") == 0) {
-			bss->wps_nfc_dev_pw_id = atoi(pos);
-			if (bss->wps_nfc_dev_pw_id < 0x10 ||
-			    bss->wps_nfc_dev_pw_id > 0xffff) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "wps_nfc_dev_pw_id value", line);
-				errors++;
-			}
-			bss->wps_nfc_pw_from_config = 1;
-		} else if (os_strcmp(buf, "wps_nfc_dh_pubkey") == 0) {
-			wpabuf_free(bss->wps_nfc_dh_pubkey);
-			bss->wps_nfc_dh_pubkey = hostapd_parse_bin(pos);
-			bss->wps_nfc_pw_from_config = 1;
-		} else if (os_strcmp(buf, "wps_nfc_dh_privkey") == 0) {
-			wpabuf_free(bss->wps_nfc_dh_privkey);
-			bss->wps_nfc_dh_privkey = hostapd_parse_bin(pos);
-			bss->wps_nfc_pw_from_config = 1;
-		} else if (os_strcmp(buf, "wps_nfc_dev_pw") == 0) {
-			wpabuf_free(bss->wps_nfc_dev_pw);
-			bss->wps_nfc_dev_pw = hostapd_parse_bin(pos);
-			bss->wps_nfc_pw_from_config = 1;
+	} else if (os_strcmp(buf, "wps_nfc_dev_pw_id") == 0) {
+		bss->wps_nfc_dev_pw_id = atoi(pos);
+		if (bss->wps_nfc_dev_pw_id < 0x10 ||
+		    bss->wps_nfc_dev_pw_id > 0xffff) {
+			wpa_printf(MSG_ERROR, "Line %d: Invalid wps_nfc_dev_pw_id value",
+				   line);
+			return 1;
+		}
+		bss->wps_nfc_pw_from_config = 1;
+	} else if (os_strcmp(buf, "wps_nfc_dh_pubkey") == 0) {
+		wpabuf_free(bss->wps_nfc_dh_pubkey);
+		bss->wps_nfc_dh_pubkey = hostapd_parse_bin(pos);
+		bss->wps_nfc_pw_from_config = 1;
+	} else if (os_strcmp(buf, "wps_nfc_dh_privkey") == 0) {
+		wpabuf_free(bss->wps_nfc_dh_privkey);
+		bss->wps_nfc_dh_privkey = hostapd_parse_bin(pos);
+		bss->wps_nfc_pw_from_config = 1;
+	} else if (os_strcmp(buf, "wps_nfc_dev_pw") == 0) {
+		wpabuf_free(bss->wps_nfc_dev_pw);
+		bss->wps_nfc_dev_pw = hostapd_parse_bin(pos);
+		bss->wps_nfc_pw_from_config = 1;
 #endif /* CONFIG_WPS_NFC */
 #endif /* CONFIG_WPS */
 #ifdef CONFIG_P2P_MANAGER
-		} else if (os_strcmp(buf, "manage_p2p") == 0) {
-			int manage = atoi(pos);
-			if (manage)
-				bss->p2p |= P2P_MANAGE;
-			else
-				bss->p2p &= ~P2P_MANAGE;
-		} else if (os_strcmp(buf, "allow_cross_connection") == 0) {
-			if (atoi(pos))
-				bss->p2p |= P2P_ALLOW_CROSS_CONNECTION;
-			else
-				bss->p2p &= ~P2P_ALLOW_CROSS_CONNECTION;
+	} else if (os_strcmp(buf, "manage_p2p") == 0) {
+		if (atoi(pos))
+			bss->p2p |= P2P_MANAGE;
+		else
+			bss->p2p &= ~P2P_MANAGE;
+	} else if (os_strcmp(buf, "allow_cross_connection") == 0) {
+		if (atoi(pos))
+			bss->p2p |= P2P_ALLOW_CROSS_CONNECTION;
+		else
+			bss->p2p &= ~P2P_ALLOW_CROSS_CONNECTION;
 #endif /* CONFIG_P2P_MANAGER */
-		} else if (os_strcmp(buf, "disassoc_low_ack") == 0) {
-			bss->disassoc_low_ack = atoi(pos);
-		} else if (os_strcmp(buf, "tdls_prohibit") == 0) {
-			int val = atoi(pos);
-			if (val)
-				bss->tdls |= TDLS_PROHIBIT;
-			else
-				bss->tdls &= ~TDLS_PROHIBIT;
-		} else if (os_strcmp(buf, "tdls_prohibit_chan_switch") == 0) {
-			int val = atoi(pos);
-			if (val)
-				bss->tdls |= TDLS_PROHIBIT_CHAN_SWITCH;
-			else
-				bss->tdls &= ~TDLS_PROHIBIT_CHAN_SWITCH;
+	} else if (os_strcmp(buf, "disassoc_low_ack") == 0) {
+		bss->disassoc_low_ack = atoi(pos);
+	} else if (os_strcmp(buf, "tdls_prohibit") == 0) {
+		if (atoi(pos))
+			bss->tdls |= TDLS_PROHIBIT;
+		else
+			bss->tdls &= ~TDLS_PROHIBIT;
+	} else if (os_strcmp(buf, "tdls_prohibit_chan_switch") == 0) {
+		if (atoi(pos))
+			bss->tdls |= TDLS_PROHIBIT_CHAN_SWITCH;
+		else
+			bss->tdls &= ~TDLS_PROHIBIT_CHAN_SWITCH;
 #ifdef CONFIG_RSN_TESTING
-		} else if (os_strcmp(buf, "rsn_testing") == 0) {
-			extern int rsn_testing;
-			rsn_testing = atoi(pos);
+	} else if (os_strcmp(buf, "rsn_testing") == 0) {
+		extern int rsn_testing;
+		rsn_testing = atoi(pos);
 #endif /* CONFIG_RSN_TESTING */
-		} else if (os_strcmp(buf, "time_advertisement") == 0) {
-			bss->time_advertisement = atoi(pos);
-		} else if (os_strcmp(buf, "time_zone") == 0) {
-			size_t tz_len = os_strlen(pos);
-			if (tz_len < 4 || tz_len > 255) {
-				wpa_printf(MSG_DEBUG, "Line %d: invalid "
-					   "time_zone", line);
-				errors++;
-				return errors;
-			}
-			os_free(bss->time_zone);
-			bss->time_zone = os_strdup(pos);
-			if (bss->time_zone == NULL)
-				errors++;
+	} else if (os_strcmp(buf, "time_advertisement") == 0) {
+		bss->time_advertisement = atoi(pos);
+	} else if (os_strcmp(buf, "time_zone") == 0) {
+		size_t tz_len = os_strlen(pos);
+		if (tz_len < 4 || tz_len > 255) {
+			wpa_printf(MSG_DEBUG, "Line %d: invalid time_zone",
+				   line);
+			return 1;
+		}
+		os_free(bss->time_zone);
+		bss->time_zone = os_strdup(pos);
+		if (bss->time_zone == NULL)
+			return 1;
 #ifdef CONFIG_WNM
-		} else if (os_strcmp(buf, "wnm_sleep_mode") == 0) {
-			bss->wnm_sleep_mode = atoi(pos);
-		} else if (os_strcmp(buf, "bss_transition") == 0) {
-			bss->bss_transition = atoi(pos);
+	} else if (os_strcmp(buf, "wnm_sleep_mode") == 0) {
+		bss->wnm_sleep_mode = atoi(pos);
+	} else if (os_strcmp(buf, "bss_transition") == 0) {
+		bss->bss_transition = atoi(pos);
 #endif /* CONFIG_WNM */
 #ifdef CONFIG_INTERWORKING
-		} else if (os_strcmp(buf, "interworking") == 0) {
-			bss->interworking = atoi(pos);
-		} else if (os_strcmp(buf, "access_network_type") == 0) {
-			bss->access_network_type = atoi(pos);
-			if (bss->access_network_type < 0 ||
-			    bss->access_network_type > 15) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "access_network_type", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "internet") == 0) {
-			bss->internet = atoi(pos);
-		} else if (os_strcmp(buf, "asra") == 0) {
-			bss->asra = atoi(pos);
-		} else if (os_strcmp(buf, "esr") == 0) {
-			bss->esr = atoi(pos);
-		} else if (os_strcmp(buf, "uesa") == 0) {
-			bss->uesa = atoi(pos);
-		} else if (os_strcmp(buf, "venue_group") == 0) {
-			bss->venue_group = atoi(pos);
-			bss->venue_info_set = 1;
-		} else if (os_strcmp(buf, "venue_type") == 0) {
-			bss->venue_type = atoi(pos);
-			bss->venue_info_set = 1;
-		} else if (os_strcmp(buf, "hessid") == 0) {
-			if (hwaddr_aton(pos, bss->hessid)) {
-				wpa_printf(MSG_ERROR, "Line %d: invalid "
-					   "hessid", line);
-				errors++;
-			}
-		} else if (os_strcmp(buf, "roaming_consortium") == 0) {
-			if (parse_roaming_consortium(bss, pos, line) < 0)
-				errors++;
-		} else if (os_strcmp(buf, "venue_name") == 0) {
-			if (parse_venue_name(bss, pos, line) < 0)
-				errors++;
-		} else if (os_strcmp(buf, "network_auth_type") == 0) {
-			u8 auth_type;
-			u16 redirect_url_len;
-			if (hexstr2bin(pos, &auth_type, 1)) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "network_auth_type '%s'",
-					   line, pos);
-				errors++;
-				return errors;
-			}
-			if (auth_type == 0 || auth_type == 2)
-				redirect_url_len = os_strlen(pos + 2);
-			else
-				redirect_url_len = 0;
-			os_free(bss->network_auth_type);
-			bss->network_auth_type =
-				os_malloc(redirect_url_len + 3 + 1);
-			if (bss->network_auth_type == NULL) {
-				errors++;
-				return errors;
-			}
-			*bss->network_auth_type = auth_type;
-			WPA_PUT_LE16(bss->network_auth_type + 1,
-				     redirect_url_len);
-			if (redirect_url_len)
-				os_memcpy(bss->network_auth_type + 3,
-					  pos + 2, redirect_url_len);
-			bss->network_auth_type_len = 3 + redirect_url_len;
-		} else if (os_strcmp(buf, "ipaddr_type_availability") == 0) {
-			if (hexstr2bin(pos, &bss->ipaddr_type_availability, 1))
-			{
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "ipaddr_type_availability '%s'",
-					   line, pos);
-				bss->ipaddr_type_configured = 0;
-				errors++;
-				return errors;
-			}
-			bss->ipaddr_type_configured = 1;
-		} else if (os_strcmp(buf, "domain_name") == 0) {
-			int j, num_domains, domain_len, domain_list_len = 0;
-			char *tok_start, *tok_prev;
-			u8 *domain_list, *domain_ptr;
+	} else if (os_strcmp(buf, "interworking") == 0) {
+		bss->interworking = atoi(pos);
+	} else if (os_strcmp(buf, "access_network_type") == 0) {
+		bss->access_network_type = atoi(pos);
+		if (bss->access_network_type < 0 ||
+		    bss->access_network_type > 15) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: invalid access_network_type",
+				   line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "internet") == 0) {
+		bss->internet = atoi(pos);
+	} else if (os_strcmp(buf, "asra") == 0) {
+		bss->asra = atoi(pos);
+	} else if (os_strcmp(buf, "esr") == 0) {
+		bss->esr = atoi(pos);
+	} else if (os_strcmp(buf, "uesa") == 0) {
+		bss->uesa = atoi(pos);
+	} else if (os_strcmp(buf, "venue_group") == 0) {
+		bss->venue_group = atoi(pos);
+		bss->venue_info_set = 1;
+	} else if (os_strcmp(buf, "venue_type") == 0) {
+		bss->venue_type = atoi(pos);
+		bss->venue_info_set = 1;
+	} else if (os_strcmp(buf, "hessid") == 0) {
+		if (hwaddr_aton(pos, bss->hessid)) {
+			wpa_printf(MSG_ERROR, "Line %d: invalid hessid", line);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "roaming_consortium") == 0) {
+		if (parse_roaming_consortium(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "venue_name") == 0) {
+		if (parse_venue_name(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "network_auth_type") == 0) {
+		u8 auth_type;
+		u16 redirect_url_len;
+		if (hexstr2bin(pos, &auth_type, 1)) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: Invalid network_auth_type '%s'",
+				   line, pos);
+			return 1;
+		}
+		if (auth_type == 0 || auth_type == 2)
+			redirect_url_len = os_strlen(pos + 2);
+		else
+			redirect_url_len = 0;
+		os_free(bss->network_auth_type);
+		bss->network_auth_type = os_malloc(redirect_url_len + 3 + 1);
+		if (bss->network_auth_type == NULL)
+			return 1;
+		*bss->network_auth_type = auth_type;
+		WPA_PUT_LE16(bss->network_auth_type + 1, redirect_url_len);
+		if (redirect_url_len)
+			os_memcpy(bss->network_auth_type + 3, pos + 2,
+				  redirect_url_len);
+		bss->network_auth_type_len = 3 + redirect_url_len;
+	} else if (os_strcmp(buf, "ipaddr_type_availability") == 0) {
+		if (hexstr2bin(pos, &bss->ipaddr_type_availability, 1)) {
+			wpa_printf(MSG_ERROR, "Line %d: Invalid ipaddr_type_availability '%s'",
+				   line, pos);
+			bss->ipaddr_type_configured = 0;
+			return 1;
+		}
+		bss->ipaddr_type_configured = 1;
+	} else if (os_strcmp(buf, "domain_name") == 0) {
+		int j, num_domains, domain_len, domain_list_len = 0;
+		char *tok_start, *tok_prev;
+		u8 *domain_list, *domain_ptr;
 
-			domain_list_len = os_strlen(pos) + 1;
-			domain_list = os_malloc(domain_list_len);
-			if (domain_list == NULL) {
-				errors++;
-				return errors;
-			}
+		domain_list_len = os_strlen(pos) + 1;
+		domain_list = os_malloc(domain_list_len);
+		if (domain_list == NULL)
+			return 1;
 
-			domain_ptr = domain_list;
-			tok_prev = pos;
-			num_domains = 1;
-			while ((tok_prev = os_strchr(tok_prev, ','))) {
-				num_domains++;
-				tok_prev++;
+		domain_ptr = domain_list;
+		tok_prev = pos;
+		num_domains = 1;
+		while ((tok_prev = os_strchr(tok_prev, ','))) {
+			num_domains++;
+			tok_prev++;
+		}
+		tok_prev = pos;
+		for (j = 0; j < num_domains; j++) {
+			tok_start = os_strchr(tok_prev, ',');
+			if (tok_start) {
+				domain_len = tok_start - tok_prev;
+				*domain_ptr = domain_len;
+				os_memcpy(domain_ptr + 1, tok_prev, domain_len);
+				domain_ptr += domain_len + 1;
+				tok_prev = ++tok_start;
+			} else {
+				domain_len = os_strlen(tok_prev);
+				*domain_ptr = domain_len;
+				os_memcpy(domain_ptr + 1, tok_prev, domain_len);
+				domain_ptr += domain_len + 1;
 			}
-			tok_prev = pos;
-			for (j = 0; j < num_domains; j++) {
-				tok_start = os_strchr(tok_prev, ',');
-				if (tok_start) {
-					domain_len = tok_start - tok_prev;
-					*domain_ptr = domain_len;
-					os_memcpy(domain_ptr + 1, tok_prev,
-						  domain_len);
-					domain_ptr += domain_len + 1;
-					tok_prev = ++tok_start;
-				} else {
-					domain_len = os_strlen(tok_prev);
-					*domain_ptr = domain_len;
-					os_memcpy(domain_ptr + 1, tok_prev,
-						  domain_len);
-					domain_ptr += domain_len + 1;
-				}
-			}
+		}
 
-			os_free(bss->domain_name);
-			bss->domain_name = domain_list;
-			bss->domain_name_len = domain_list_len;
-		} else if (os_strcmp(buf, "anqp_3gpp_cell_net") == 0) {
-			if (parse_3gpp_cell_net(bss, pos, line) < 0)
-				errors++;
-		} else if (os_strcmp(buf, "nai_realm") == 0) {
-			if (parse_nai_realm(bss, pos, line) < 0)
-				errors++;
-		} else if (os_strcmp(buf, "gas_frag_limit") == 0) {
-			bss->gas_frag_limit = atoi(pos);
-		} else if (os_strcmp(buf, "gas_comeback_delay") == 0) {
-			bss->gas_comeback_delay = atoi(pos);
-		} else if (os_strcmp(buf, "qos_map_set") == 0) {
-			if (parse_qos_map_set(bss, pos, line) < 0)
-				errors++;
+		os_free(bss->domain_name);
+		bss->domain_name = domain_list;
+		bss->domain_name_len = domain_list_len;
+	} else if (os_strcmp(buf, "anqp_3gpp_cell_net") == 0) {
+		if (parse_3gpp_cell_net(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "nai_realm") == 0) {
+		if (parse_nai_realm(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "gas_frag_limit") == 0) {
+		bss->gas_frag_limit = atoi(pos);
+	} else if (os_strcmp(buf, "gas_comeback_delay") == 0) {
+		bss->gas_comeback_delay = atoi(pos);
+	} else if (os_strcmp(buf, "qos_map_set") == 0) {
+		if (parse_qos_map_set(bss, pos, line) < 0)
+			return 1;
 #endif /* CONFIG_INTERWORKING */
 #ifdef CONFIG_RADIUS_TEST
-		} else if (os_strcmp(buf, "dump_msk_file") == 0) {
-			os_free(bss->dump_msk_file);
-			bss->dump_msk_file = os_strdup(pos);
+	} else if (os_strcmp(buf, "dump_msk_file") == 0) {
+		os_free(bss->dump_msk_file);
+		bss->dump_msk_file = os_strdup(pos);
 #endif /* CONFIG_RADIUS_TEST */
 #ifdef CONFIG_HS20
-		} else if (os_strcmp(buf, "hs20") == 0) {
-			bss->hs20 = atoi(pos);
-		} else if (os_strcmp(buf, "disable_dgaf") == 0) {
-			bss->disable_dgaf = atoi(pos);
-		} else if (os_strcmp(buf, "osen") == 0) {
-			bss->osen = atoi(pos);
-		} else if (os_strcmp(buf, "anqp_domain_id") == 0) {
-			bss->anqp_domain_id = atoi(pos);
-		} else if (os_strcmp(buf, "hs20_deauth_req_timeout") == 0) {
-			bss->hs20_deauth_req_timeout = atoi(pos);
-		} else if (os_strcmp(buf, "hs20_oper_friendly_name") == 0) {
-			if (hs20_parse_oper_friendly_name(bss, pos, line) < 0)
-				errors++;
-		} else if (os_strcmp(buf, "hs20_wan_metrics") == 0) {
-			if (hs20_parse_wan_metrics(bss, pos, line) < 0) {
-				errors++;
-				return errors;
-			}
-		} else if (os_strcmp(buf, "hs20_conn_capab") == 0) {
-			if (hs20_parse_conn_capab(bss, pos, line) < 0) {
-				errors++;
-				return errors;
-			}
-		} else if (os_strcmp(buf, "hs20_operating_class") == 0) {
-			u8 *oper_class;
-			size_t oper_class_len;
-			oper_class_len = os_strlen(pos);
-			if (oper_class_len < 2 || (oper_class_len & 0x01)) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "hs20_operating_class '%s'",
-					   line, pos);
-				errors++;
-				return errors;
-			}
-			oper_class_len /= 2;
-			oper_class = os_malloc(oper_class_len);
-			if (oper_class == NULL) {
-				errors++;
-				return errors;
-			}
-			if (hexstr2bin(pos, oper_class, oper_class_len)) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "hs20_operating_class '%s'",
-					   line, pos);
-				os_free(oper_class);
-				errors++;
-				return errors;
-			}
-			os_free(bss->hs20_operating_class);
-			bss->hs20_operating_class = oper_class;
-			bss->hs20_operating_class_len = oper_class_len;
-		} else if (os_strcmp(buf, "hs20_icon") == 0) {
-			if (hs20_parse_icon(bss, pos) < 0) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "hs20_icon '%s'", line, pos);
-				errors++;
-				return errors;
-			}
-		} else if (os_strcmp(buf, "osu_ssid") == 0) {
-			if (hs20_parse_osu_ssid(bss, pos, line) < 0)
-				errors++;
-		} else if (os_strcmp(buf, "osu_server_uri") == 0) {
-			if (hs20_parse_osu_server_uri(bss, pos, line) < 0)
-				errors++;
-		} else if (os_strcmp(buf, "osu_friendly_name") == 0) {
-			if (hs20_parse_osu_friendly_name(bss, pos, line) < 0)
-				errors++;
-		} else if (os_strcmp(buf, "osu_nai") == 0) {
-			if (hs20_parse_osu_nai(bss, pos, line) < 0)
-				errors++;
-		} else if (os_strcmp(buf, "osu_method_list") == 0) {
-			if (hs20_parse_osu_method_list(bss, pos, line) < 0)
-				errors++;
-		} else if (os_strcmp(buf, "osu_icon") == 0) {
-			if (hs20_parse_osu_icon(bss, pos, line) < 0)
-				errors++;
-		} else if (os_strcmp(buf, "osu_service_desc") == 0) {
-			if (hs20_parse_osu_service_desc(bss, pos, line) < 0)
-				errors++;
-		} else if (os_strcmp(buf, "subscr_remediation_url") == 0) {
-			os_free(bss->subscr_remediation_url);
-			bss->subscr_remediation_url = os_strdup(pos);
-		} else if (os_strcmp(buf, "subscr_remediation_method") == 0) {
-			bss->subscr_remediation_method = atoi(pos);
+	} else if (os_strcmp(buf, "hs20") == 0) {
+		bss->hs20 = atoi(pos);
+	} else if (os_strcmp(buf, "disable_dgaf") == 0) {
+		bss->disable_dgaf = atoi(pos);
+	} else if (os_strcmp(buf, "osen") == 0) {
+		bss->osen = atoi(pos);
+	} else if (os_strcmp(buf, "anqp_domain_id") == 0) {
+		bss->anqp_domain_id = atoi(pos);
+	} else if (os_strcmp(buf, "hs20_deauth_req_timeout") == 0) {
+		bss->hs20_deauth_req_timeout = atoi(pos);
+	} else if (os_strcmp(buf, "hs20_oper_friendly_name") == 0) {
+		if (hs20_parse_oper_friendly_name(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "hs20_wan_metrics") == 0) {
+		if (hs20_parse_wan_metrics(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "hs20_conn_capab") == 0) {
+		if (hs20_parse_conn_capab(bss, pos, line) < 0) {
+			return 1;
+		}
+	} else if (os_strcmp(buf, "hs20_operating_class") == 0) {
+		u8 *oper_class;
+		size_t oper_class_len;
+		oper_class_len = os_strlen(pos);
+		if (oper_class_len < 2 || (oper_class_len & 0x01)) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: Invalid hs20_operating_class '%s'",
+				   line, pos);
+			return 1;
+		}
+		oper_class_len /= 2;
+		oper_class = os_malloc(oper_class_len);
+		if (oper_class == NULL)
+			return 1;
+		if (hexstr2bin(pos, oper_class, oper_class_len)) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: Invalid hs20_operating_class '%s'",
+				   line, pos);
+			os_free(oper_class);
+			return 1;
+		}
+		os_free(bss->hs20_operating_class);
+		bss->hs20_operating_class = oper_class;
+		bss->hs20_operating_class_len = oper_class_len;
+	} else if (os_strcmp(buf, "hs20_icon") == 0) {
+		if (hs20_parse_icon(bss, pos) < 0) {
+			wpa_printf(MSG_ERROR, "Line %d: Invalid hs20_icon '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "osu_ssid") == 0) {
+		if (hs20_parse_osu_ssid(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "osu_server_uri") == 0) {
+		if (hs20_parse_osu_server_uri(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "osu_friendly_name") == 0) {
+		if (hs20_parse_osu_friendly_name(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "osu_nai") == 0) {
+		if (hs20_parse_osu_nai(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "osu_method_list") == 0) {
+		if (hs20_parse_osu_method_list(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "osu_icon") == 0) {
+		if (hs20_parse_osu_icon(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "osu_service_desc") == 0) {
+		if (hs20_parse_osu_service_desc(bss, pos, line) < 0)
+			return 1;
+	} else if (os_strcmp(buf, "subscr_remediation_url") == 0) {
+		os_free(bss->subscr_remediation_url);
+		bss->subscr_remediation_url = os_strdup(pos);
+	} else if (os_strcmp(buf, "subscr_remediation_method") == 0) {
+		bss->subscr_remediation_method = atoi(pos);
 #endif /* CONFIG_HS20 */
 #ifdef CONFIG_TESTING_OPTIONS
-#define PARSE_TEST_PROBABILITY(_val)					\
-		} else if (os_strcmp(buf, #_val) == 0) {		\
-			char *end;					\
-									\
-			conf->_val = strtod(pos, &end);			\
-			if (*end || conf->_val < 0.0d ||		\
-			    conf->_val > 1.0d) {			\
-				wpa_printf(MSG_ERROR,			\
-					   "Line %d: Invalid value '%s'", \
-					   line, pos);			\
-				errors++;				\
-				return errors;				\
-			}
-		PARSE_TEST_PROBABILITY(ignore_probe_probability)
-		PARSE_TEST_PROBABILITY(ignore_auth_probability)
-		PARSE_TEST_PROBABILITY(ignore_assoc_probability)
-		PARSE_TEST_PROBABILITY(ignore_reassoc_probability)
-		PARSE_TEST_PROBABILITY(corrupt_gtk_rekey_mic_probability)
-		} else if (os_strcmp(buf, "bss_load_test") == 0) {
-			WPA_PUT_LE16(bss->bss_load_test, atoi(pos));
-			pos = os_strchr(pos, ':');
-			if (pos == NULL) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "bss_load_test", line);
-				return 1;
-			}
-			pos++;
-			bss->bss_load_test[2] = atoi(pos);
-			pos = os_strchr(pos, ':');
-			if (pos == NULL) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "bss_load_test", line);
-				return 1;
-			}
-			pos++;
-			WPA_PUT_LE16(&bss->bss_load_test[3], atoi(pos));
-			bss->bss_load_test_set = 1;
-#endif /* CONFIG_TESTING_OPTIONS */
-		} else if (os_strcmp(buf, "vendor_elements") == 0) {
-			struct wpabuf *elems;
-			size_t len = os_strlen(pos);
-			if (len & 0x01) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "vendor_elements '%s'", line, pos);
-				return 1;
-			}
-			len /= 2;
-			if (len == 0) {
-				wpabuf_free(bss->vendor_elements);
-				bss->vendor_elements = NULL;
-				return 0;
-			}
-
-			elems = wpabuf_alloc(len);
-			if (elems == NULL)
-				return 1;
-
-			if (hexstr2bin(pos, wpabuf_put(elems, len), len)) {
-				wpabuf_free(elems);
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "vendor_elements '%s'", line, pos);
-				return 1;
-			}
-
-			wpabuf_free(bss->vendor_elements);
-			bss->vendor_elements = elems;
-		} else if (os_strcmp(buf, "sae_anti_clogging_threshold") == 0) {
-			bss->sae_anti_clogging_threshold = atoi(pos);
-		} else if (os_strcmp(buf, "sae_groups") == 0) {
-			if (hostapd_parse_intlist(&bss->sae_groups, pos)) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid "
-					   "sae_groups value '%s'", line, pos);
-				return 1;
-			}
-		} else if (os_strcmp(buf, "local_pwr_constraint") == 0) {
-			int val = atoi(pos);
-			if (val < 0 || val > 255) {
-				wpa_printf(MSG_ERROR, "Line %d: Invalid local_pwr_constraint %d (expected 0..255)",
-					   line, val);
-				return 1;
-			}
-			conf->local_pwr_constraint = val;
-		} else if (os_strcmp(buf, "spectrum_mgmt_required") == 0) {
-			conf->spectrum_mgmt_required = atoi(pos);
-		} else {
-			wpa_printf(MSG_ERROR, "Line %d: unknown configuration "
-				   "item '%s'", line, buf);
-			errors++;
+#define PARSE_TEST_PROBABILITY(_val)				\
+	} else if (os_strcmp(buf, #_val) == 0) {		\
+		char *end;					\
+								\
+		conf->_val = strtod(pos, &end);			\
+		if (*end || conf->_val < 0.0d ||		\
+		    conf->_val > 1.0d) {			\
+			wpa_printf(MSG_ERROR,			\
+				   "Line %d: Invalid value '%s'", \
+				   line, pos);			\
+			return 1;				\
 		}
+	PARSE_TEST_PROBABILITY(ignore_probe_probability)
+	PARSE_TEST_PROBABILITY(ignore_auth_probability)
+	PARSE_TEST_PROBABILITY(ignore_assoc_probability)
+	PARSE_TEST_PROBABILITY(ignore_reassoc_probability)
+	PARSE_TEST_PROBABILITY(corrupt_gtk_rekey_mic_probability)
+	} else if (os_strcmp(buf, "bss_load_test") == 0) {
+		WPA_PUT_LE16(bss->bss_load_test, atoi(pos));
+		pos = os_strchr(pos, ':');
+		if (pos == NULL) {
+			wpa_printf(MSG_ERROR, "Line %d: Invalid bss_load_test",
+				   line);
+			return 1;
+		}
+		pos++;
+		bss->bss_load_test[2] = atoi(pos);
+		pos = os_strchr(pos, ':');
+		if (pos == NULL) {
+			wpa_printf(MSG_ERROR, "Line %d: Invalid bss_load_test",
+				   line);
+			return 1;
+		}
+		pos++;
+		WPA_PUT_LE16(&bss->bss_load_test[3], atoi(pos));
+		bss->bss_load_test_set = 1;
+#endif /* CONFIG_TESTING_OPTIONS */
+	} else if (os_strcmp(buf, "vendor_elements") == 0) {
+		struct wpabuf *elems;
+		size_t len = os_strlen(pos);
+		if (len & 0x01) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: Invalid vendor_elements '%s'",
+				   line, pos);
+			return 1;
+		}
+		len /= 2;
+		if (len == 0) {
+			wpabuf_free(bss->vendor_elements);
+			bss->vendor_elements = NULL;
+			return 0;
+		}
+
+		elems = wpabuf_alloc(len);
+		if (elems == NULL)
+			return 1;
+
+		if (hexstr2bin(pos, wpabuf_put(elems, len), len)) {
+			wpabuf_free(elems);
+			wpa_printf(MSG_ERROR,
+				   "Line %d: Invalid vendor_elements '%s'",
+				   line, pos);
+			return 1;
+		}
+
+		wpabuf_free(bss->vendor_elements);
+		bss->vendor_elements = elems;
+	} else if (os_strcmp(buf, "sae_anti_clogging_threshold") == 0) {
+		bss->sae_anti_clogging_threshold = atoi(pos);
+	} else if (os_strcmp(buf, "sae_groups") == 0) {
+		if (hostapd_parse_intlist(&bss->sae_groups, pos)) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: Invalid sae_groups value '%s'",
+				   line, pos);
+			return 1;
+		}
+	} else if (os_strcmp(buf, "local_pwr_constraint") == 0) {
+		int val = atoi(pos);
+		if (val < 0 || val > 255) {
+			wpa_printf(MSG_ERROR, "Line %d: Invalid local_pwr_constraint %d (expected 0..255)",
+				   line, val);
+			return 1;
+		}
+		conf->local_pwr_constraint = val;
+	} else if (os_strcmp(buf, "spectrum_mgmt_required") == 0) {
+		conf->spectrum_mgmt_required = atoi(pos);
+	} else {
+		wpa_printf(MSG_ERROR,
+			   "Line %d: unknown configuration item '%s'",
+			   line, buf);
+		return 1;
 	}
 
-	return errors;
+	return 0;
 }
 
 
diff --git a/hs20/client/Android.mk b/hs20/client/Android.mk
new file mode 100644
index 0000000..b7bd932
--- /dev/null
+++ b/hs20/client/Android.mk
@@ -0,0 +1,73 @@
+LOCAL_PATH := $(call my-dir)
+
+INCLUDES = $(LOCAL_PATH)
+INCLUDES += $(LOCAL_PATH)/../../src/utils
+INCLUDES += $(LOCAL_PATH)/../../src/common
+INCLUDES += $(LOCAL_PATH)/../../src
+INCLUDES += external/openssl/include
+INCLUDES += external/libxml2/include
+INCLUDES += external/curl/include
+INCLUDES += external/webkit/Source/WebKit/gtk
+INCLUDES += external/icu4c/common
+
+
+#GTKCFLAGS := $(shell pkg-config --cflags gtk+-2.0 webkit-1.0)
+#GTKLIBS := $(shell pkg-config --libs gtk+-2.0 webkit-1.0)
+#CFLAGS += $(GTKCFLAGS)
+#LIBS += $(GTKLIBS)
+
+L_CFLAGS += -DCONFIG_CTRL_IFACE
+L_CFLAGS += -DCONFIG_CTRL_IFACE_UNIX
+L_CFLAGS += -DCONFIG_CTRL_IFACE_CLIENT_DIR=\"/data/misc/wifi/sockets\"
+L_CFLAGS += -DLIBXML_SCHEMAS_ENABLED
+L_CFLAGS += -DLIBXML_REGEXP_ENABLED
+
+OBJS = spp_client.c
+OBJS += oma_dm_client.c
+OBJS += osu_client.c
+OBJS += est.c
+OBJS += ../../src/common/wpa_ctrl.c
+OBJS += ../../src/common/wpa_helpers.c
+OBJS += ../../src/utils/xml-utils.c
+#OBJS += ../../src/utils/browser-android.c
+OBJS += ../../src/utils/browser-wpadebug.c
+OBJS += ../../src/utils/wpabuf.c
+OBJS += ../../src/utils/eloop.c
+OBJS += ../../src/wps/httpread.c
+OBJS += ../../src/wps/http_server.c
+OBJS += ../../src/utils/xml_libxml2.c
+OBJS += ../../src/utils/http_curl.c
+OBJS += ../../src/utils/base64.c
+OBJS += ../../src/utils/os_unix.c
+L_CFLAGS += -DCONFIG_DEBUG_FILE
+OBJS += ../../src/utils/wpa_debug.c
+OBJS += ../../src/utils/common.c
+OBJS += ../../src/crypto/crypto_internal.c
+OBJS += ../../src/crypto/md5-internal.c
+OBJS += ../../src/crypto/sha1-internal.c
+OBJS += ../../src/crypto/sha256-internal.c
+
+L_CFLAGS += -DEAP_TLS_OPENSSL
+
+#CFLAGS += $(shell xml2-config --cflags)
+#LIBS += $(shell xml2-config --libs)
+
+
+########################
+include $(CLEAR_VARS)
+LOCAL_MODULE := hs20-osu-client
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SHARED_LIBRARIES := libc libcutils
+LOCAL_SHARED_LIBRARIES += libcrypto libssl
+#LOCAL_SHARED_LIBRARIES += libxml2
+LOCAL_STATIC_LIBRARIES += libxml2
+LOCAL_SHARED_LIBRARIES += libicuuc
+LOCAL_SHARED_LIBRARIES += libcurl
+
+LOCAL_CFLAGS := $(L_CFLAGS)
+LOCAL_SRC_FILES := $(OBJS)
+LOCAL_C_INCLUDES := $(INCLUDES)
+include $(BUILD_EXECUTABLE)
+
+########################
diff --git a/hs20/client/Makefile b/hs20/client/Makefile
new file mode 100644
index 0000000..ca67b54
--- /dev/null
+++ b/hs20/client/Makefile
@@ -0,0 +1,94 @@
+all: hs20-osu-client
+
+ifndef CC
+CC=gcc
+endif
+
+ifndef LDO
+LDO=$(CC)
+endif
+
+Q=@
+E=echo
+ifeq ($(V), 1)
+Q=
+E=true
+endif
+
+ifndef CFLAGS
+CFLAGS = -MMD -O2 -Wall -g
+endif
+
+CFLAGS += -I../../src/utils
+CFLAGS += -I../../src/common
+CFLAGS += -I../../src
+
+ifndef CONFIG_NO_BROWSER
+ifndef CONFIG_BROWSER_SYSTEM
+GTKCFLAGS := $(shell pkg-config --cflags gtk+-3.0 webkitgtk-3.0)
+GTKLIBS := $(shell pkg-config --libs gtk+-3.0 webkitgtk-3.0)
+CFLAGS += $(GTKCFLAGS)
+LIBS += $(GTKLIBS)
+endif
+endif
+
+OBJS=spp_client.o
+OBJS += oma_dm_client.o
+OBJS += osu_client.o
+OBJS += est.o
+OBJS += ../../src/utils/xml-utils.o
+CFLAGS += -DCONFIG_CTRL_IFACE
+CFLAGS += -DCONFIG_CTRL_IFACE_UNIX
+OBJS += ../../src/common/wpa_ctrl.o ../../src/common/wpa_helpers.o
+ifdef CONFIG_NO_BROWSER
+CFLAGS += -DCONFIG_NO_BROWSER
+else
+ifdef CONFIG_BROWSER_SYSTEM
+OBJS += ../../src/utils/eloop.o
+OBJS += ../../src/utils/wpabuf.o
+OBJS += ../../src/wps/httpread.o
+OBJS += ../../src/wps/http_server.o
+OBJS += ../../src/utils/browser-system.o
+else
+OBJS += ../../src/utils/browser.o
+endif
+endif
+OBJS += ../../src/utils/xml_libxml2.o
+OBJS += ../../src/utils/http_curl.o
+OBJS += ../../src/utils/base64.o
+OBJS += ../../src/utils/os_unix.o
+CFLAGS += -DCONFIG_DEBUG_FILE
+OBJS += ../../src/utils/wpa_debug.o
+OBJS += ../../src/utils/common.o
+OBJS += ../../src/crypto/crypto_internal.o
+OBJS += ../../src/crypto/md5-internal.o
+OBJS += ../../src/crypto/sha1-internal.o
+OBJS += ../../src/crypto/sha256-internal.o
+
+CFLAGS += $(shell xml2-config --cflags)
+LIBS += $(shell xml2-config --libs)
+LIBS += -lcurl
+
+CFLAGS += -DEAP_TLS_OPENSSL
+LIBS += -lssl -lcrypto
+
+hs20-osu-client: $(OBJS)
+	$(Q)$(LDO) $(LDFLAGS) -o hs20-osu-client $(OBJS) $(LIBS)
+	@$(E) "  LD " $@
+
+%.o: %.c
+	$(Q)$(CC) -c -o $@ $(CFLAGS) $<
+	@$(E) "  CC " $<
+
+clean:
+	rm -f core *~ *.o *.d hs20-osu-client
+	rm -f ../../src/utils/*.o
+	rm -f ../../src/utils/*.d
+	rm -f ../../src/common/*.o
+	rm -f ../../src/common/*.d
+	rm -f ../../src/crypto/*.o
+	rm -f ../../src/crypto/*.d
+	rm -f ../../src/wps/*.o
+	rm -f ../../src/wps/*.d
+
+-include $(OBJS:%.o=%.d)
diff --git a/hs20/client/devdetail.xml b/hs20/client/devdetail.xml
new file mode 100644
index 0000000..6d0389e
--- /dev/null
+++ b/hs20/client/devdetail.xml
@@ -0,0 +1,47 @@
+<DevDetail xmlns="urn:oma:mo:oma-dm-devdetail:1.0">
+	<Ext>
+		<org.wi-fi>
+			<Wi-Fi>
+				<EAPMethodList>
+					<EAPMethod1>
+						<EAPType>13</EAPType>
+					</EAPMethod1>
+					<EAPMethod2>
+						<EAPType>21</EAPType>
+						<InnerMethod>MS-CHAP-V2</InnerMethod>
+					</EAPMethod2>
+					<EAPMethod3>
+						<EAPType>18</EAPType>
+					</EAPMethod3>
+					<EAPMethod4>
+						<EAPType>23</EAPType>
+					</EAPMethod4>
+					<EAPMethod5>
+						<EAPType>50</EAPType>
+					</EAPMethod5>
+				</EAPMethodList>
+				<ManufacturingCertificate>false</ManufacturingCertificate>
+				<Wi-FiMACAddress>020102030405</Wi-FiMACAddress>
+				<IMSI>310026000000000</IMSI>
+				<IMEI_MEID>imei:490123456789012</IMEI_MEID>
+				<ClientTriggerRedirectURI>http://localhost:12345/</ClientTriggerRedirectURI>
+				<Ops>
+					<launchBrowserToURI></launchBrowserToURI>
+					<negotiateClientCertTLS></negotiateClientCertTLS>
+					<getCertificate></getCertificate>
+				</Ops>
+			</Wi-Fi>
+		</org.wi-fi>
+	</Ext>
+	<URI>
+		<MaxDepth>0</MaxDepth>
+		<MaxTotLen>0</MaxTotLen>
+		<MaxSegLen>0</MaxSegLen>
+	</URI>
+	<DevType>MobilePhone</DevType>
+	<OEM>Manufacturer</OEM>
+	<FwV>1.0</FwV>
+	<SwV>1.0</SwV>
+	<HwV>1.0</HwV>
+	<LrgObj>false</LrgObj>
+</DevDetail>
diff --git a/hs20/client/devinfo.xml b/hs20/client/devinfo.xml
new file mode 100644
index 0000000..d48a520
--- /dev/null
+++ b/hs20/client/devinfo.xml
@@ -0,0 +1,7 @@
+<DevInfo xmlns="urn:oma:mo:oma-dm-devinfo:1.0">
+	<DevId>urn:Example:HS20-station:123456</DevId>
+	<Man>Manufacturer</Man>
+	<Mod>HS20-station</Mod>
+	<DmV>1.2</DmV>
+	<Lang>en</Lang>
+</DevInfo>
diff --git a/hs20/client/est.c b/hs20/client/est.c
new file mode 100644
index 0000000..c1d514a
--- /dev/null
+++ b/hs20/client/est.c
@@ -0,0 +1,700 @@
+/*
+ * Hotspot 2.0 OSU client - EST client
+ * Copyright (c) 2012-2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/pem.h>
+#include <openssl/pkcs7.h>
+#include <openssl/rsa.h>
+#include <openssl/asn1.h>
+#include <openssl/asn1t.h>
+#include <openssl/x509.h>
+#include <openssl/x509v3.h>
+
+#include "common.h"
+#include "utils/base64.h"
+#include "utils/xml-utils.h"
+#include "utils/http-utils.h"
+#include "osu_client.h"
+
+
+static int pkcs7_to_cert(struct hs20_osu_client *ctx, const u8 *pkcs7,
+			 size_t len, char *pem_file, char *der_file)
+{
+	PKCS7 *p7 = NULL;
+	const unsigned char *p = pkcs7;
+	STACK_OF(X509) *certs;
+	int i, num, ret = -1;
+	BIO *out = NULL;
+
+	p7 = d2i_PKCS7(NULL, &p, len);
+	if (p7 == NULL) {
+		wpa_printf(MSG_INFO, "Could not parse PKCS#7 object: %s",
+			   ERR_error_string(ERR_get_error(), NULL));
+		write_result(ctx, "Could not parse PKCS#7 object from EST");
+		goto fail;
+	}
+
+	switch (OBJ_obj2nid(p7->type)) {
+	case NID_pkcs7_signed:
+		certs = p7->d.sign->cert;
+		break;
+	case NID_pkcs7_signedAndEnveloped:
+		certs = p7->d.signed_and_enveloped->cert;
+		break;
+	default:
+		certs = NULL;
+		break;
+	}
+
+	if (!certs || ((num = sk_X509_num(certs)) == 0)) {
+		wpa_printf(MSG_INFO, "No certificates found in PKCS#7 object");
+		write_result(ctx, "No certificates found in PKCS#7 object");
+		goto fail;
+	}
+
+	if (der_file) {
+		FILE *f = fopen(der_file, "wb");
+		if (f == NULL)
+			goto fail;
+		i2d_X509_fp(f, sk_X509_value(certs, 0));
+		fclose(f);
+	}
+
+	if (pem_file) {
+		out = BIO_new(BIO_s_file());
+		if (out == NULL ||
+		    BIO_write_filename(out, pem_file) <= 0)
+			goto fail;
+
+		for (i = 0; i < num; i++) {
+			X509 *cert = sk_X509_value(certs, i);
+			X509_print(out, cert);
+			PEM_write_bio_X509(out, cert);
+			BIO_puts(out, "\n");
+		}
+	}
+
+	ret = 0;
+
+fail:
+	PKCS7_free(p7);
+	if (out)
+		BIO_free_all(out);
+
+	return ret;
+}
+
+
+int est_load_cacerts(struct hs20_osu_client *ctx, const char *url)
+{
+	char *buf, *resp;
+	size_t buflen;
+	unsigned char *pkcs7;
+	size_t pkcs7_len, resp_len;
+	int res;
+
+	buflen = os_strlen(url) + 100;
+	buf = os_malloc(buflen);
+	if (buf == NULL)
+		return -1;
+
+	os_snprintf(buf, buflen, "%s/cacerts", url);
+	wpa_printf(MSG_INFO, "Download EST cacerts from %s", buf);
+	write_summary(ctx, "Download EST cacerts from %s", buf);
+	res = http_download_file(ctx->http, buf, "Cert/est-cacerts.txt",
+				 ctx->ca_fname);
+	if (res < 0) {
+		wpa_printf(MSG_INFO, "Failed to download EST cacerts from %s",
+			   buf);
+		write_result(ctx, "Failed to download EST cacerts from %s",
+			     buf);
+		os_free(buf);
+		return -1;
+	}
+	os_free(buf);
+
+	resp = os_readfile("Cert/est-cacerts.txt", &resp_len);
+	if (resp == NULL) {
+		wpa_printf(MSG_INFO, "Could not read Cert/est-cacerts.txt");
+		write_result(ctx, "Could not read EST cacerts");
+		return -1;
+	}
+
+	pkcs7 = base64_decode((unsigned char *) resp, resp_len, &pkcs7_len);
+	if (pkcs7 && pkcs7_len < resp_len / 2) {
+		wpa_printf(MSG_INFO, "Too short base64 decode (%u bytes; downloaded %u bytes) - assume this was binary",
+			   (unsigned int) pkcs7_len, (unsigned int) resp_len);
+		os_free(pkcs7);
+		pkcs7 = NULL;
+	}
+	if (pkcs7 == NULL) {
+		wpa_printf(MSG_INFO, "EST workaround - Could not decode base64, assume this is DER encoded PKCS7");
+		pkcs7 = os_malloc(resp_len);
+		if (pkcs7) {
+			os_memcpy(pkcs7, resp, resp_len);
+			pkcs7_len = resp_len;
+		}
+	}
+	os_free(resp);
+
+	if (pkcs7 == NULL) {
+		wpa_printf(MSG_INFO, "Could not fetch PKCS7 cacerts");
+		write_result(ctx, "Could not fetch EST PKCS#7 cacerts");
+		return -1;
+	}
+
+	res = pkcs7_to_cert(ctx, pkcs7, pkcs7_len, "Cert/est-cacerts.pem",
+			    NULL);
+	os_free(pkcs7);
+	if (res < 0) {
+		wpa_printf(MSG_INFO, "Could not parse CA certs from PKCS#7 cacerts response");
+		write_result(ctx, "Could not parse CA certs from EST PKCS#7 cacerts response");
+		return -1;
+	}
+	unlink("Cert/est-cacerts.txt");
+
+	return 0;
+}
+
+
+/*
+ * CsrAttrs ::= SEQUENCE SIZE (0..MAX) OF AttrOrOID
+ *
+ * AttrOrOID ::= CHOICE {
+ *   oid OBJECT IDENTIFIER,
+ *   attribute Attribute }
+ *
+ * Attribute ::= SEQUENCE {
+ *   type OBJECT IDENTIFIER,
+ *   values SET SIZE(1..MAX) OF OBJECT IDENTIFIER }
+ */
+
+typedef struct {
+	ASN1_OBJECT *type;
+	STACK_OF(ASN1_OBJECT) *values;
+} Attribute;
+
+typedef struct {
+	int type;
+	union {
+		ASN1_OBJECT *oid;
+		Attribute *attribute;
+	} d;
+} AttrOrOID;
+
+typedef struct {
+	int type;
+	STACK_OF(AttrOrOID) *attrs;
+} CsrAttrs;
+
+ASN1_SEQUENCE(Attribute) = {
+	ASN1_SIMPLE(Attribute, type, ASN1_OBJECT),
+	ASN1_SET_OF(Attribute, values, ASN1_OBJECT)
+} ASN1_SEQUENCE_END(Attribute);
+
+ASN1_CHOICE(AttrOrOID) = {
+	ASN1_SIMPLE(AttrOrOID, d.oid, ASN1_OBJECT),
+	ASN1_SIMPLE(AttrOrOID, d.attribute, Attribute)
+} ASN1_CHOICE_END(AttrOrOID);
+
+ASN1_CHOICE(CsrAttrs) = {
+	ASN1_SEQUENCE_OF(CsrAttrs, attrs, AttrOrOID)
+} ASN1_CHOICE_END(CsrAttrs);
+
+IMPLEMENT_ASN1_FUNCTIONS(CsrAttrs);
+
+
+static void add_csrattrs_oid(struct hs20_osu_client *ctx, ASN1_OBJECT *oid,
+			     STACK_OF(X509_EXTENSION) *exts)
+{
+	char txt[100];
+	int res;
+
+	if (!oid)
+		return;
+
+	res = OBJ_obj2txt(txt, sizeof(txt), oid, 1);
+	if (res < 0 || res >= (int) sizeof(txt))
+		return;
+
+	if (os_strcmp(txt, "1.2.840.113549.1.9.7") == 0) {
+		wpa_printf(MSG_INFO, "TODO: csrattr challengePassword");
+	} else if (os_strcmp(txt, "1.2.840.113549.1.1.11") == 0) {
+		wpa_printf(MSG_INFO, "csrattr sha256WithRSAEncryption");
+	} else {
+		wpa_printf(MSG_INFO, "Ignore unsupported csrattr oid %s", txt);
+	}
+}
+
+
+static void add_csrattrs_ext_req(struct hs20_osu_client *ctx,
+				 STACK_OF(ASN1_OBJECT) *values,
+				 STACK_OF(X509_EXTENSION) *exts)
+{
+	char txt[100];
+	int i, num, res;
+
+	num = sk_ASN1_OBJECT_num(values);
+	for (i = 0; i < num; i++) {
+		ASN1_OBJECT *oid = sk_ASN1_OBJECT_value(values, i);
+
+		res = OBJ_obj2txt(txt, sizeof(txt), oid, 1);
+		if (res < 0 || res >= (int) sizeof(txt))
+			continue;
+
+		if (os_strcmp(txt, "1.3.6.1.1.1.1.22") == 0) {
+			wpa_printf(MSG_INFO, "TODO: extReq macAddress");
+		} else if (os_strcmp(txt, "1.3.6.1.4.1.40808.1.1.3") == 0) {
+			wpa_printf(MSG_INFO, "TODO: extReq imei");
+		} else if (os_strcmp(txt, "1.3.6.1.4.1.40808.1.1.4") == 0) {
+			wpa_printf(MSG_INFO, "TODO: extReq meid");
+		} else if (os_strcmp(txt, "1.3.6.1.4.1.40808.1.1.5") == 0) {
+			wpa_printf(MSG_INFO, "TODO: extReq DevId");
+		} else {
+			wpa_printf(MSG_INFO, "Ignore unsupported cstattr extensionsRequest %s",
+				   txt);
+		}
+	}
+}
+
+
+static void add_csrattrs_attr(struct hs20_osu_client *ctx, Attribute *attr,
+			      STACK_OF(X509_EXTENSION) *exts)
+{
+	char txt[100], txt2[100];
+	int i, num, res;
+
+	if (!attr || !attr->type || !attr->values)
+		return;
+
+	res = OBJ_obj2txt(txt, sizeof(txt), attr->type, 1);
+	if (res < 0 || res >= (int) sizeof(txt))
+		return;
+
+	if (os_strcmp(txt, "1.2.840.113549.1.9.14") == 0) {
+		add_csrattrs_ext_req(ctx, attr->values, exts);
+		return;
+	}
+
+	num = sk_ASN1_OBJECT_num(attr->values);
+	for (i = 0; i < num; i++) {
+		ASN1_OBJECT *oid = sk_ASN1_OBJECT_value(attr->values, i);
+
+		res = OBJ_obj2txt(txt2, sizeof(txt2), oid, 1);
+		if (res < 0 || res >= (int) sizeof(txt2))
+			continue;
+
+		wpa_printf(MSG_INFO, "Ignore unsupported cstattr::attr %s oid %s",
+			   txt, txt2);
+	}
+}
+
+
+static void add_csrattrs(struct hs20_osu_client *ctx, CsrAttrs *csrattrs,
+			 STACK_OF(X509_EXTENSION) *exts)
+{
+	int i, num;
+
+	if (!csrattrs || ! csrattrs->attrs)
+		return;
+
+	num = SKM_sk_num(AttrOrOID, csrattrs->attrs);
+	for (i = 0; i < num; i++) {
+		AttrOrOID *ao = SKM_sk_value(AttrOrOID, csrattrs->attrs, i);
+		switch (ao->type) {
+		case 0:
+			add_csrattrs_oid(ctx, ao->d.oid, exts);
+			break;
+		case 1:
+			add_csrattrs_attr(ctx, ao->d.attribute, exts);
+			break;
+		}
+	}
+}
+
+
+static int generate_csr(struct hs20_osu_client *ctx, char *key_pem,
+			char *csr_pem, char *est_req, char *old_cert,
+			CsrAttrs *csrattrs)
+{
+	EVP_PKEY_CTX *pctx = NULL;
+	EVP_PKEY *pkey = NULL;
+	RSA *rsa;
+	X509_REQ *req = NULL;
+	int ret = -1;
+	unsigned int val;
+	X509_NAME *subj = NULL;
+	char name[100];
+	STACK_OF(X509_EXTENSION) *exts = NULL;
+	X509_EXTENSION *ex;
+	BIO *out;
+
+	wpa_printf(MSG_INFO, "Generate RSA private key");
+	write_summary(ctx, "Generate RSA private key");
+	pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
+	if (!pctx)
+		return -1;
+
+	if (EVP_PKEY_keygen_init(pctx) <= 0)
+		goto fail;
+
+	if (EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, 2048) <= 0)
+		goto fail;
+
+	if (EVP_PKEY_keygen(pctx, &pkey) <= 0)
+		goto fail;
+	EVP_PKEY_CTX_free(pctx);
+	pctx = NULL;
+
+	rsa = EVP_PKEY_get1_RSA(pkey);
+	if (rsa == NULL)
+		goto fail;
+
+	if (key_pem) {
+		FILE *f = fopen(key_pem, "wb");
+		if (f == NULL)
+			goto fail;
+		if (!PEM_write_RSAPrivateKey(f, rsa, NULL, NULL, 0, NULL,
+					     NULL)) {
+			wpa_printf(MSG_INFO, "Could not write private key: %s",
+				   ERR_error_string(ERR_get_error(), NULL));
+			fclose(f);
+			goto fail;
+		}
+		fclose(f);
+	}
+
+	wpa_printf(MSG_INFO, "Generate CSR");
+	write_summary(ctx, "Generate CSR");
+	req = X509_REQ_new();
+	if (req == NULL)
+		goto fail;
+
+	if (old_cert) {
+		FILE *f;
+		X509 *cert;
+		int res;
+
+		f = fopen(old_cert, "r");
+		if (f == NULL)
+			goto fail;
+		cert = PEM_read_X509(f, NULL, NULL, NULL);
+		fclose(f);
+
+		if (cert == NULL)
+			goto fail;
+		res = X509_REQ_set_subject_name(req,
+						X509_get_subject_name(cert));
+		X509_free(cert);
+		if (!res)
+			goto fail;
+	} else {
+		os_get_random((u8 *) &val, sizeof(val));
+		os_snprintf(name, sizeof(name), "cert-user-%u", val);
+		subj = X509_NAME_new();
+		if (subj == NULL ||
+		    !X509_NAME_add_entry_by_txt(subj, "CN", MBSTRING_ASC,
+						(unsigned char *) name,
+						-1, -1, 0) ||
+		    !X509_REQ_set_subject_name(req, subj))
+			goto fail;
+		X509_NAME_free(subj);
+		subj = NULL;
+	}
+
+	if (!X509_REQ_set_pubkey(req, pkey))
+		goto fail;
+
+	exts = sk_X509_EXTENSION_new_null();
+	if (!exts)
+		goto fail;
+
+	ex = X509V3_EXT_conf_nid(NULL, NULL, NID_basic_constraints,
+				 "CA:FALSE");
+	if (ex == NULL ||
+	    !sk_X509_EXTENSION_push(exts, ex))
+		goto fail;
+
+	ex = X509V3_EXT_conf_nid(NULL, NULL, NID_key_usage,
+				 "nonRepudiation,digitalSignature,keyEncipherment");
+	if (ex == NULL ||
+	    !sk_X509_EXTENSION_push(exts, ex))
+		goto fail;
+
+	ex = X509V3_EXT_conf_nid(NULL, NULL, NID_ext_key_usage,
+				 "1.3.6.1.4.1.40808.1.1.2");
+	if (ex == NULL ||
+	    !sk_X509_EXTENSION_push(exts, ex))
+		goto fail;
+
+	add_csrattrs(ctx, csrattrs, exts);
+
+	if (!X509_REQ_add_extensions(req, exts))
+		goto fail;
+	sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
+	exts = NULL;
+
+	if (!X509_REQ_sign(req, pkey, EVP_sha256()))
+		goto fail;
+
+	out = BIO_new(BIO_s_mem());
+	if (out) {
+		char *txt;
+		size_t rlen;
+
+		X509_REQ_print(out, req);
+		rlen = BIO_ctrl_pending(out);
+		txt = os_malloc(rlen + 1);
+		if (txt) {
+			int res = BIO_read(out, txt, rlen);
+			if (res > 0) {
+				txt[res] = '\0';
+				wpa_printf(MSG_MSGDUMP, "OpenSSL: Certificate request:\n%s",
+					   txt);
+			}
+			os_free(txt);
+		}
+		BIO_free(out);
+	}
+
+	if (csr_pem) {
+		FILE *f = fopen(csr_pem, "w");
+		if (f == NULL)
+			goto fail;
+		X509_REQ_print_fp(f, req);
+		if (!PEM_write_X509_REQ(f, req)) {
+			fclose(f);
+			goto fail;
+		}
+		fclose(f);
+	}
+
+	if (est_req) {
+		BIO *mem = BIO_new(BIO_s_mem());
+		BUF_MEM *ptr;
+		char *pos, *end, *buf_end;
+		FILE *f;
+
+		if (mem == NULL)
+			goto fail;
+		if (!PEM_write_bio_X509_REQ(mem, req)) {
+			BIO_free(mem);
+			goto fail;
+		}
+
+		BIO_get_mem_ptr(mem, &ptr);
+		pos = ptr->data;
+		buf_end = pos + ptr->length;
+
+		/* Remove START/END lines */
+		while (pos < buf_end && *pos != '\n')
+			pos++;
+		if (pos == buf_end) {
+			BIO_free(mem);
+			goto fail;
+		}
+		pos++;
+
+		end = pos;
+		while (end < buf_end && *end != '-')
+			end++;
+
+		f = fopen(est_req, "w");
+		if (f == NULL) {
+			BIO_free(mem);
+			goto fail;
+		}
+		fwrite(pos, end - pos, 1, f);
+		fclose(f);
+
+		BIO_free(mem);
+	}
+
+	ret = 0;
+fail:
+	if (exts)
+		sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
+	if (subj)
+		X509_NAME_free(subj);
+	if (req)
+		X509_REQ_free(req);
+	if (pkey)
+		EVP_PKEY_free(pkey);
+	if (pctx)
+		EVP_PKEY_CTX_free(pctx);
+	return ret;
+}
+
+
+int est_build_csr(struct hs20_osu_client *ctx, const char *url)
+{
+	char *buf;
+	size_t buflen;
+	int res;
+	char old_cert_buf[200];
+	char *old_cert = NULL;
+	CsrAttrs *csrattrs = NULL;
+
+	buflen = os_strlen(url) + 100;
+	buf = os_malloc(buflen);
+	if (buf == NULL)
+		return -1;
+
+	os_snprintf(buf, buflen, "%s/csrattrs", url);
+	wpa_printf(MSG_INFO, "Download csrattrs from %s", buf);
+	write_summary(ctx, "Download EST csrattrs from %s", buf);
+	res = http_download_file(ctx->http, buf, "Cert/est-csrattrs.txt",
+				 ctx->ca_fname);
+	os_free(buf);
+	if (res < 0) {
+		wpa_printf(MSG_INFO, "Failed to download EST csrattrs - assume no extra attributes are needed");
+	} else {
+		size_t resp_len;
+		char *resp;
+		unsigned char *attrs;
+		const unsigned char *pos;
+		size_t attrs_len;
+
+		resp = os_readfile("Cert/est-csrattrs.txt", &resp_len);
+		if (resp == NULL) {
+			wpa_printf(MSG_INFO, "Could not read csrattrs");
+			return -1;
+		}
+
+		attrs = base64_decode((unsigned char *) resp, resp_len,
+				      &attrs_len);
+		os_free(resp);
+
+		if (attrs == NULL) {
+			wpa_printf(MSG_INFO, "Could not base64 decode csrattrs");
+			return -1;
+		}
+		unlink("Cert/est-csrattrs.txt");
+
+		pos = attrs;
+		csrattrs = d2i_CsrAttrs(NULL, &pos, attrs_len);
+		os_free(attrs);
+		if (csrattrs == NULL) {
+			wpa_printf(MSG_INFO, "Failed to parse csrattrs ASN.1");
+			/* Continue assuming no additional requirements */
+		}
+	}
+
+	if (ctx->client_cert_present) {
+		os_snprintf(old_cert_buf, sizeof(old_cert_buf),
+			    "SP/%s/client-cert.pem", ctx->fqdn);
+		old_cert = old_cert_buf;
+	}
+
+	res = generate_csr(ctx, "Cert/privkey-plain.pem", "Cert/est-req.pem",
+			   "Cert/est-req.b64", old_cert, csrattrs);
+	if (csrattrs)
+		CsrAttrs_free(csrattrs);
+
+	return res;
+}
+
+
+int est_simple_enroll(struct hs20_osu_client *ctx, const char *url,
+		      const char *user, const char *pw)
+{
+	char *buf, *resp, *req, *req2;
+	size_t buflen, resp_len, len, pkcs7_len;
+	unsigned char *pkcs7;
+	FILE *f;
+	char client_cert_buf[200];
+	char client_key_buf[200];
+	const char *client_cert = NULL, *client_key = NULL;
+	int res;
+
+	req = os_readfile("Cert/est-req.b64", &len);
+	if (req == NULL) {
+		wpa_printf(MSG_INFO, "Could not read Cert/req.b64");
+		return -1;
+	}
+	req2 = os_realloc(req, len + 1);
+	if (req2 == NULL) {
+		os_free(req);
+		return -1;
+	}
+	req2[len] = '\0';
+	req = req2;
+	wpa_printf(MSG_DEBUG, "EST simpleenroll request: %s", req);
+
+	buflen = os_strlen(url) + 100;
+	buf = os_malloc(buflen);
+	if (buf == NULL) {
+		os_free(req);
+		return -1;
+	}
+
+	if (ctx->client_cert_present) {
+		os_snprintf(buf, buflen, "%s/simplereenroll", url);
+		os_snprintf(client_cert_buf, sizeof(client_cert_buf),
+			    "SP/%s/client-cert.pem", ctx->fqdn);
+		client_cert = client_cert_buf;
+		os_snprintf(client_key_buf, sizeof(client_key_buf),
+			    "SP/%s/client-key.pem", ctx->fqdn);
+		client_key = client_key_buf;
+	} else
+		os_snprintf(buf, buflen, "%s/simpleenroll", url);
+	wpa_printf(MSG_INFO, "EST simpleenroll URL: %s", buf);
+	write_summary(ctx, "EST simpleenroll URL: %s", buf);
+	resp = http_post(ctx->http, buf, req, "application/pkcs10",
+			 "Content-Transfer-Encoding: base64",
+			 ctx->ca_fname, user, pw, client_cert, client_key,
+			 &resp_len);
+	os_free(buf);
+	if (resp == NULL) {
+		wpa_printf(MSG_INFO, "EST certificate enrollment failed");
+		write_result(ctx, "EST certificate enrollment failed");
+		return -1;
+	}
+	wpa_printf(MSG_DEBUG, "EST simpleenroll response: %s", resp);
+	f = fopen("Cert/est-resp.raw", "w");
+	if (f) {
+		fwrite(resp, resp_len, 1, f);
+		fclose(f);
+	}
+
+	pkcs7 = base64_decode((unsigned char *) resp, resp_len, &pkcs7_len);
+	if (pkcs7 == NULL) {
+		wpa_printf(MSG_INFO, "EST workaround - Could not decode base64, assume this is DER encoded PKCS7");
+		pkcs7 = os_malloc(resp_len);
+		if (pkcs7) {
+			os_memcpy(pkcs7, resp, resp_len);
+			pkcs7_len = resp_len;
+		}
+	}
+	os_free(resp);
+
+	if (pkcs7 == NULL) {
+		wpa_printf(MSG_INFO, "Failed to parse simpleenroll base64 response");
+		write_result(ctx, "Failed to parse EST simpleenroll base64 response");
+		return -1;
+	}
+
+	res = pkcs7_to_cert(ctx, pkcs7, pkcs7_len, "Cert/est_cert.pem",
+			    "Cert/est_cert.der");
+	os_free(pkcs7);
+
+	if (res < 0) {
+		wpa_printf(MSG_INFO, "EST: Failed to extract certificate from PKCS7 file");
+		write_result(ctx, "EST: Failed to extract certificate from EST PKCS7 file");
+		return -1;
+	}
+
+	wpa_printf(MSG_INFO, "EST simple%senroll completed successfully",
+		   ctx->client_cert_present ? "re" : "");
+	write_summary(ctx, "EST simple%senroll completed successfully",
+		      ctx->client_cert_present ? "re" : "");
+
+	return 0;
+}
diff --git a/hs20/client/oma_dm_client.c b/hs20/client/oma_dm_client.c
new file mode 100644
index 0000000..8fc350b
--- /dev/null
+++ b/hs20/client/oma_dm_client.c
@@ -0,0 +1,1374 @@
+/*
+ * Hotspot 2.0 - OMA DM client
+ * Copyright (c) 2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+
+#include "common.h"
+#include "wpa_helpers.h"
+#include "xml-utils.h"
+#include "http-utils.h"
+#include "utils/browser.h"
+#include "osu_client.h"
+
+
+#define DM_SERVER_INITIATED_MGMT 1200
+#define DM_CLIENT_INITIATED_MGMT 1201
+#define DM_GENERIC_ALERT 1226
+
+/* OMA-TS-SyncML-RepPro-V1_2_2 - 10. Response Status Codes */
+#define DM_RESP_OK 200
+#define DM_RESP_AUTH_ACCEPTED 212
+#define DM_RESP_CHUNKED_ITEM_ACCEPTED 213
+#define DM_RESP_NOT_EXECUTED 215
+#define DM_RESP_ATOMIC_ROLL_BACK_OK 216
+#define DM_RESP_NOT_MODIFIED 304
+#define DM_RESP_BAD_REQUEST 400
+#define DM_RESP_UNAUTHORIZED 401
+#define DM_RESP_FORBIDDEN 403
+#define DM_RESP_NOT_FOUND 404
+#define DM_RESP_COMMAND_NOT_ALLOWED 405
+#define DM_RESP_OPTIONAL_FEATURE_NOT_SUPPORTED 406
+#define DM_RESP_MISSING_CREDENTIALS 407
+#define DM_RESP_CONFLICT 409
+#define DM_RESP_GONE 410
+#define DM_RESP_INCOMPLETE_COMMAND 412
+#define DM_RESP_REQ_ENTITY_TOO_LARGE 413
+#define DM_RESP_URI_TOO_LONG 414
+#define DM_RESP_UNSUPPORTED_MEDIA_TYPE_OR_FORMAT 415
+#define DM_RESP_REQ_TOO_BIG 416
+#define DM_RESP_ALREADY_EXISTS 418
+#define DM_RESP_DEVICE_FULL 420
+#define DM_RESP_SIZE_MISMATCH 424
+#define DM_RESP_PERMISSION_DENIED 425
+#define DM_RESP_COMMAND_FAILED 500
+#define DM_RESP_COMMAND_NOT_IMPLEMENTED 501
+#define DM_RESP_ATOMIC_ROLL_BACK_FAILED 516
+
+#define DM_HS20_SUBSCRIPTION_CREATION \
+	"org.wi-fi.hotspot2dot0.SubscriptionCreation"
+#define DM_HS20_SUBSCRIPTION_PROVISIONING \
+	"org.wi-fi.hotspot2dot0.SubscriptionProvisioning"
+#define DM_HS20_SUBSCRIPTION_REMEDIATION \
+	"org.wi-fi.hotspot2dot0.SubscriptionRemediation"
+#define DM_HS20_POLICY_UPDATE \
+	"org.wi-fi.hotspot2dot0.PolicyUpdate"
+
+#define DM_URI_PPS "./Wi-Fi/org.wi-fi/PerProviderSubscription"
+#define DM_URI_LAUNCH_BROWSER \
+	"./DevDetail/Ext/org.wi-fi/Wi-Fi/Ops/launchBrowserToURI"
+
+
+static void add_item(struct hs20_osu_client *ctx, xml_node_t *parent,
+		     const char *locuri, const char *data);
+
+
+static const char * int2str(int val)
+{
+	static char buf[20];
+	snprintf(buf, sizeof(buf), "%d", val);
+	return buf;
+}
+
+
+static char * oma_dm_get_target_locuri(struct hs20_osu_client *ctx,
+				       xml_node_t *node)
+{
+	xml_node_t *locuri;
+	char *uri, *ret = NULL;
+
+	locuri = get_node(ctx->xml, node, "Item/Target/LocURI");
+	if (locuri == NULL)
+		return NULL;
+
+	uri = xml_node_get_text(ctx->xml, locuri);
+	if (uri)
+		ret = os_strdup(uri);
+	xml_node_get_text_free(ctx->xml, uri);
+	return ret;
+}
+
+
+static void oma_dm_add_locuri(struct hs20_osu_client *ctx, xml_node_t *parent,
+			      const char *element, const char *uri)
+{
+	xml_node_t *node;
+
+	node = xml_node_create(ctx->xml, parent, NULL, element);
+	if (node == NULL)
+		return;
+	xml_node_create_text(ctx->xml, node, NULL, "LocURI", uri);
+}
+
+
+static xml_node_t * oma_dm_build_hdr(struct hs20_osu_client *ctx,
+				     const char *url, int msgid)
+{
+	xml_node_t *syncml, *synchdr;
+	xml_namespace_t *ns;
+
+	syncml = xml_node_create_root(ctx->xml, "SYNCML:SYNCML1.2", NULL, &ns,
+				      "SyncML");
+
+	synchdr = xml_node_create(ctx->xml, syncml, NULL, "SyncHdr");
+	xml_node_create_text(ctx->xml, synchdr, NULL, "VerDTD", "1.2");
+	xml_node_create_text(ctx->xml, synchdr, NULL, "VerProto", "DM/1.2");
+	xml_node_create_text(ctx->xml, synchdr, NULL, "SessionID", "1");
+	xml_node_create_text(ctx->xml, synchdr, NULL, "MsgID", int2str(msgid));
+
+	oma_dm_add_locuri(ctx, synchdr, "Target", url);
+	oma_dm_add_locuri(ctx, synchdr, "Source", ctx->devid);
+
+	return syncml;
+}
+
+
+static void oma_dm_add_cmdid(struct hs20_osu_client *ctx, xml_node_t *parent,
+			     int cmdid)
+{
+	xml_node_create_text(ctx->xml, parent, NULL, "CmdID", int2str(cmdid));
+}
+
+
+static xml_node_t * add_alert(struct hs20_osu_client *ctx, xml_node_t *parent,
+			      int cmdid, int data)
+{
+	xml_node_t *node;
+
+	node = xml_node_create(ctx->xml, parent, NULL, "Alert");
+	if (node == NULL)
+		return NULL;
+	oma_dm_add_cmdid(ctx, node, cmdid);
+	xml_node_create_text(ctx->xml, node, NULL, "Data", int2str(data));
+
+	return node;
+}
+
+
+static xml_node_t * add_status(struct hs20_osu_client *ctx, xml_node_t *parent,
+			       int msgref, int cmdref, int cmdid,
+			       const char *cmd, int data, const char *targetref)
+{
+	xml_node_t *node;
+
+	node = xml_node_create(ctx->xml, parent, NULL, "Status");
+	if (node == NULL)
+		return NULL;
+	oma_dm_add_cmdid(ctx, node, cmdid);
+	xml_node_create_text(ctx->xml, node, NULL, "MsgRef", int2str(msgref));
+	if (cmdref)
+		xml_node_create_text(ctx->xml, node, NULL, "CmdRef",
+				     int2str(cmdref));
+	xml_node_create_text(ctx->xml, node, NULL, "Cmd", cmd);
+	xml_node_create_text(ctx->xml, node, NULL, "Data", int2str(data));
+	if (targetref) {
+		xml_node_create_text(ctx->xml, node, NULL, "TargetRef",
+				     targetref);
+	}
+
+	return node;
+}
+
+
+static xml_node_t * add_results(struct hs20_osu_client *ctx, xml_node_t *parent,
+				int msgref, int cmdref, int cmdid,
+				const char *locuri, const char *data)
+{
+	xml_node_t *node;
+
+	node = xml_node_create(ctx->xml, parent, NULL, "Results");
+	if (node == NULL)
+		return NULL;
+
+	oma_dm_add_cmdid(ctx, node, cmdid);
+	xml_node_create_text(ctx->xml, node, NULL, "MsgRef", int2str(msgref));
+	xml_node_create_text(ctx->xml, node, NULL, "CmdRef", int2str(cmdref));
+	add_item(ctx, node, locuri, data);
+
+	return node;
+}
+
+
+static char * mo_str(struct hs20_osu_client *ctx, const char *urn,
+		     const char *fname)
+{
+	xml_node_t *fnode, *tnds;
+	char *str;
+
+	fnode = node_from_file(ctx->xml, fname);
+	if (!fnode)
+		return NULL;
+	tnds = mo_to_tnds(ctx->xml, fnode, 0, urn, "syncml:dmddf1.2");
+	xml_node_free(ctx->xml, fnode);
+	if (!tnds)
+		return NULL;
+
+	str = xml_node_to_str(ctx->xml, tnds);
+	xml_node_free(ctx->xml, tnds);
+	if (str == NULL)
+		return NULL;
+	wpa_printf(MSG_INFO, "MgmtTree: %s", str);
+
+	return str;
+}
+
+
+static void add_item(struct hs20_osu_client *ctx, xml_node_t *parent,
+		     const char *locuri, const char *data)
+{
+	xml_node_t *item, *node;
+
+	item = xml_node_create(ctx->xml, parent, NULL, "Item");
+	oma_dm_add_locuri(ctx, item, "Source", locuri);
+	node = xml_node_create(ctx->xml, item, NULL, "Meta");
+	xml_node_create_text_ns(ctx->xml, node, "syncml:metinf", "Format",
+				"Chr");
+	xml_node_create_text_ns(ctx->xml, node, "syncml:metinf", "Type",
+				"text/plain");
+	xml_node_create_text(ctx->xml, item, NULL, "Data", data);
+}
+
+
+static void add_replace_devinfo(struct hs20_osu_client *ctx, xml_node_t *parent,
+				int cmdid)
+{
+	xml_node_t *info, *child, *replace;
+	const char *name;
+	char locuri[200], *txt;
+
+	info = node_from_file(ctx->xml, "devinfo.xml");
+	if (info == NULL) {
+		wpa_printf(MSG_INFO, "Could not read devinfo.xml");
+		return;
+	}
+
+	replace = xml_node_create(ctx->xml, parent, NULL, "Replace");
+	if (replace == NULL) {
+		xml_node_free(ctx->xml, info);
+		return;
+	}
+	oma_dm_add_cmdid(ctx, replace, cmdid);
+
+	xml_node_for_each_child(ctx->xml, child, info) {
+		xml_node_for_each_check(ctx->xml, child);
+		name = xml_node_get_localname(ctx->xml, child);
+		os_snprintf(locuri, sizeof(locuri), "./DevInfo/%s", name);
+		txt = xml_node_get_text(ctx->xml, child);
+		if (txt) {
+			add_item(ctx, replace, locuri, txt);
+			xml_node_get_text_free(ctx->xml, txt);
+		}
+	}
+
+	xml_node_free(ctx->xml, info);
+}
+
+
+static void oma_dm_add_hs20_generic_alert(struct hs20_osu_client *ctx,
+					  xml_node_t *syncbody,
+					  int cmdid, const char *oper,
+					  const char *data)
+{
+	xml_node_t *node, *item;
+	char buf[200];
+
+	node = add_alert(ctx, syncbody, cmdid, DM_GENERIC_ALERT);
+
+	item = xml_node_create(ctx->xml, node, NULL, "Item");
+	oma_dm_add_locuri(ctx, item, "Source", DM_URI_PPS);
+	node = xml_node_create(ctx->xml, item, NULL, "Meta");
+	snprintf(buf, sizeof(buf), "Reversed-Domain-Name: %s", oper);
+	xml_node_create_text_ns(ctx->xml, node, "syncml:metinf", "Type", buf);
+	xml_node_create_text_ns(ctx->xml, node, "syncml:metinf", "Format",
+				"xml");
+	xml_node_create_text(ctx->xml, item, NULL, "Data", data);
+}
+
+
+static xml_node_t * build_oma_dm_1(struct hs20_osu_client *ctx,
+				   const char *url, int msgid, const char *oper)
+{
+	xml_node_t *syncml, *syncbody;
+	char *str;
+	int cmdid = 0;
+
+	syncml = oma_dm_build_hdr(ctx, url, msgid);
+	if (syncml == NULL)
+		return NULL;
+
+	syncbody = xml_node_create(ctx->xml, syncml, NULL, "SyncBody");
+	if (syncbody == NULL) {
+		xml_node_free(ctx->xml, syncml);
+		return NULL;
+	}
+
+	cmdid++;
+	add_alert(ctx, syncbody, cmdid, DM_CLIENT_INITIATED_MGMT);
+
+	str = mo_str(ctx, NULL, "devdetail.xml");
+	if (str == NULL) {
+		xml_node_free(ctx->xml, syncml);
+		return NULL;
+	}
+	cmdid++;
+	oma_dm_add_hs20_generic_alert(ctx, syncbody, cmdid, oper, str);
+	os_free(str);
+
+	cmdid++;
+	add_replace_devinfo(ctx, syncbody, cmdid);
+
+	xml_node_create(ctx->xml, syncbody, NULL, "Final");
+
+	return syncml;
+}
+
+
+static xml_node_t * build_oma_dm_1_sub_reg(struct hs20_osu_client *ctx,
+					   const char *url, int msgid)
+{
+	xml_node_t *syncml;
+
+	syncml = build_oma_dm_1(ctx, url, msgid, DM_HS20_SUBSCRIPTION_CREATION);
+	if (syncml)
+		debug_dump_node(ctx, "OMA-DM Package 1 (sub reg)", syncml);
+
+	return syncml;
+}
+
+
+static xml_node_t * build_oma_dm_1_sub_prov(struct hs20_osu_client *ctx,
+					    const char *url, int msgid)
+{
+	xml_node_t *syncml;
+
+	syncml = build_oma_dm_1(ctx, url, msgid,
+				DM_HS20_SUBSCRIPTION_PROVISIONING);
+	if (syncml)
+		debug_dump_node(ctx, "OMA-DM Package 1 (sub prov)", syncml);
+
+	return syncml;
+}
+
+
+static xml_node_t * build_oma_dm_1_pol_upd(struct hs20_osu_client *ctx,
+					   const char *url, int msgid)
+{
+	xml_node_t *syncml;
+
+	syncml = build_oma_dm_1(ctx, url, msgid, DM_HS20_POLICY_UPDATE);
+	if (syncml)
+		debug_dump_node(ctx, "OMA-DM Package 1 (pol upd)", syncml);
+
+	return syncml;
+}
+
+
+static xml_node_t * build_oma_dm_1_sub_rem(struct hs20_osu_client *ctx,
+					   const char *url, int msgid)
+{
+	xml_node_t *syncml;
+
+	syncml = build_oma_dm_1(ctx, url, msgid,
+				DM_HS20_SUBSCRIPTION_REMEDIATION);
+	if (syncml)
+		debug_dump_node(ctx, "OMA-DM Package 1 (sub rem)", syncml);
+
+	return syncml;
+}
+
+
+static int oma_dm_exec_browser(struct hs20_osu_client *ctx, xml_node_t *exec)
+{
+	xml_node_t *node;
+	char *data;
+	int res;
+
+	node = get_node(ctx->xml, exec, "Item/Data");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "No Data node found");
+		return DM_RESP_BAD_REQUEST;
+	}
+
+	data = xml_node_get_text(ctx->xml, node);
+	wpa_printf(MSG_INFO, "Data: %s", data);
+	wpa_printf(MSG_INFO, "Launch browser to URI '%s'", data);
+	write_summary(ctx, "Launch browser to URI '%s'", data);
+	res = hs20_web_browser(data);
+	xml_node_get_text_free(ctx->xml, data);
+	if (res > 0) {
+		wpa_printf(MSG_INFO, "User response in browser completed successfully");
+		write_summary(ctx, "User response in browser completed successfully");
+		return DM_RESP_OK;
+	} else {
+		wpa_printf(MSG_INFO, "Failed to receive user response");
+		write_summary(ctx, "Failed to receive user response");
+		return DM_RESP_COMMAND_FAILED;
+	}
+}
+
+
+static int oma_dm_exec_get_cert(struct hs20_osu_client *ctx, xml_node_t *exec)
+{
+	xml_node_t *node, *getcert;
+	char *data;
+	const char *name;
+	int res;
+
+	wpa_printf(MSG_INFO, "Client certificate enrollment");
+	write_summary(ctx, "Client certificate enrollment");
+
+	node = get_node(ctx->xml, exec, "Item/Data");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "No Data node found");
+		return DM_RESP_BAD_REQUEST;
+	}
+
+	data = xml_node_get_text(ctx->xml, node);
+	wpa_printf(MSG_INFO, "Data: %s", data);
+	getcert = xml_node_from_buf(ctx->xml, data);
+	xml_node_get_text_free(ctx->xml, data);
+
+	if (getcert == NULL) {
+		wpa_printf(MSG_INFO, "Could not parse Item/Data node contents");
+		return DM_RESP_BAD_REQUEST;
+	}
+
+	debug_dump_node(ctx, "OMA-DM getCertificate", getcert);
+
+	name = xml_node_get_localname(ctx->xml, getcert);
+	if (name == NULL || os_strcasecmp(name, "getCertificate") != 0) {
+		wpa_printf(MSG_INFO, "Unexpected getCertificate node name '%s'",
+			   name);
+		return DM_RESP_BAD_REQUEST;
+	}
+
+	res = osu_get_certificate(ctx, getcert);
+
+	xml_node_free(ctx->xml, getcert);
+
+	return res == 0 ? DM_RESP_OK : DM_RESP_COMMAND_FAILED;
+}
+
+
+static int oma_dm_exec(struct hs20_osu_client *ctx, xml_node_t *exec)
+{
+	char *locuri;
+	int ret;
+
+	locuri = oma_dm_get_target_locuri(ctx, exec);
+	if (locuri == NULL) {
+		wpa_printf(MSG_INFO, "No Target LocURI node found");
+		return DM_RESP_BAD_REQUEST;
+	}
+
+	wpa_printf(MSG_INFO, "Target LocURI: %s", locuri);
+
+	if (os_strcasecmp(locuri, "./DevDetail/Ext/org.wi-fi/Wi-Fi/Ops/"
+			  "launchBrowserToURI") == 0) {
+		ret = oma_dm_exec_browser(ctx, exec);
+	} else if (os_strcasecmp(locuri, "./DevDetail/Ext/org.wi-fi/Wi-Fi/Ops/"
+			  "getCertificate") == 0) {
+		ret = oma_dm_exec_get_cert(ctx, exec);
+	} else {
+		wpa_printf(MSG_INFO, "Unsupported exec Target LocURI");
+		ret = DM_RESP_NOT_FOUND;
+	}
+	os_free(locuri);
+
+	return ret;
+}
+
+
+static int oma_dm_run_add(struct hs20_osu_client *ctx, const char *locuri,
+			  xml_node_t *add, xml_node_t *pps,
+			  const char *pps_fname)
+{
+	const char *pos;
+	size_t fqdn_len;
+	xml_node_t *node, *tnds, *unode, *pps_node;
+	char *data, *uri, *upos, *end;
+	int use_tnds = 0;
+	size_t uri_len;
+
+	wpa_printf(MSG_INFO, "Add command target LocURI: %s", locuri);
+
+	if (os_strncasecmp(locuri, "./Wi-Fi/", 8) != 0) {
+		wpa_printf(MSG_INFO, "Do not allow Add outside ./Wi-Fi");
+		return DM_RESP_PERMISSION_DENIED;
+	}
+	pos = locuri + 8;
+
+	if (ctx->fqdn == NULL)
+		return DM_RESP_COMMAND_FAILED;
+	fqdn_len = os_strlen(ctx->fqdn);
+	if (os_strncasecmp(pos, ctx->fqdn, fqdn_len) != 0 ||
+	    pos[fqdn_len] != '/') {
+		wpa_printf(MSG_INFO, "Do not allow Add outside ./Wi-Fi/%s",
+			   ctx->fqdn);
+		return DM_RESP_PERMISSION_DENIED;
+	}
+	pos += fqdn_len + 1;
+
+	if (os_strncasecmp(pos, "PerProviderSubscription/", 24) != 0) {
+		wpa_printf(MSG_INFO,
+			   "Do not allow Add outside ./Wi-Fi/%s/PerProviderSubscription",
+			   ctx->fqdn);
+		return DM_RESP_PERMISSION_DENIED;
+	}
+	pos += 24;
+
+	wpa_printf(MSG_INFO, "Add command for PPS node %s", pos);
+
+	pps_node = get_node(ctx->xml, pps, pos);
+	if (pps_node) {
+		wpa_printf(MSG_INFO, "Specified PPS node exists already");
+		return DM_RESP_ALREADY_EXISTS;
+	}
+
+	uri = os_strdup(pos);
+	if (uri == NULL)
+		return DM_RESP_COMMAND_FAILED;
+	while (!pps_node) {
+		upos = os_strrchr(uri, '/');
+		if (!upos)
+			break;
+		upos[0] = '\0';
+		pps_node = get_node(ctx->xml, pps, uri);
+		wpa_printf(MSG_INFO, "Node %s %s", uri,
+			   pps_node ? "exists" : "does not exist");
+	}
+
+	wpa_printf(MSG_INFO, "Parent URI: %s", uri);
+
+	if (!pps_node) {
+		/* Add at root of PPS MO */
+		pps_node = pps;
+	}
+
+	uri_len = os_strlen(uri);
+	os_strlcpy(uri, pos + uri_len, os_strlen(pos));
+	upos = uri;
+	while (*upos == '/')
+		upos++;
+	wpa_printf(MSG_INFO, "Nodes to add: %s", upos);
+
+	for (;;) {
+		end = os_strchr(upos, '/');
+		if (!end)
+			break;
+		*end = '\0';
+		wpa_printf(MSG_INFO, "Adding interim node %s", upos);
+		pps_node = xml_node_create(ctx->xml, pps_node, NULL, upos);
+		if (pps_node == NULL) {
+			os_free(uri);
+			return DM_RESP_COMMAND_FAILED;
+		}
+		upos = end + 1;
+	}
+
+	wpa_printf(MSG_INFO, "Adding node %s", upos);
+
+	node = get_node(ctx->xml, add, "Item/Meta/Type");
+	if (node) {
+		char *type;
+		type = xml_node_get_text(ctx->xml, node);
+		use_tnds = node &&
+			os_strstr(type, "application/vnd.syncml.dmtnds+xml");
+	}
+
+	node = get_node(ctx->xml, add, "Item/Data");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "No Add/Item/Data found");
+		os_free(uri);
+		return DM_RESP_BAD_REQUEST;
+	}
+
+	data = xml_node_get_text(ctx->xml, node);
+	if (data == NULL) {
+		wpa_printf(MSG_INFO, "Could not get Add/Item/Data text");
+		os_free(uri);
+		return DM_RESP_BAD_REQUEST;
+	}
+
+	wpa_printf(MSG_DEBUG, "Add/Item/Data: %s", data);
+
+	if (use_tnds) {
+		tnds = xml_node_from_buf(ctx->xml, data);
+		xml_node_get_text_free(ctx->xml, data);
+		if (tnds == NULL) {
+			wpa_printf(MSG_INFO,
+				   "Could not parse Add/Item/Data text");
+			os_free(uri);
+			return DM_RESP_BAD_REQUEST;
+		}
+
+		unode = tnds_to_mo(ctx->xml, tnds);
+		xml_node_free(ctx->xml, tnds);
+		if (unode == NULL) {
+			wpa_printf(MSG_INFO, "Could not parse TNDS text");
+			os_free(uri);
+			return DM_RESP_BAD_REQUEST;
+		}
+
+		debug_dump_node(ctx, "Parsed TNDS", unode);
+
+		xml_node_add_child(ctx->xml, pps_node, unode);
+	} else {
+		/* TODO: What to do here? */
+		os_free(uri);
+		return DM_RESP_BAD_REQUEST;
+	}
+
+	os_free(uri);
+
+	if (update_pps_file(ctx, pps_fname, pps) < 0)
+		return DM_RESP_COMMAND_FAILED;
+
+	ctx->pps_updated = 1;
+
+	return DM_RESP_OK;
+}
+
+
+static int oma_dm_add(struct hs20_osu_client *ctx, xml_node_t *add,
+		      xml_node_t *pps, const char *pps_fname)
+{
+	xml_node_t *node;
+	char *locuri;
+	char fname[300];
+	int ret;
+
+	node = get_node(ctx->xml, add, "Item/Target/LocURI");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "No Target LocURI node found");
+		return DM_RESP_BAD_REQUEST;
+	}
+	locuri = xml_node_get_text(ctx->xml, node);
+	wpa_printf(MSG_INFO, "Target LocURI: %s", locuri);
+	if (os_strncasecmp(locuri, "./Wi-Fi/", 8) != 0) {
+		wpa_printf(MSG_INFO, "Unsupported Add Target LocURI");
+		xml_node_get_text_free(ctx->xml, locuri);
+		return DM_RESP_PERMISSION_DENIED;
+	}
+
+	node = get_node(ctx->xml, add, "Item/Data");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "No Data node found");
+		xml_node_get_text_free(ctx->xml, locuri);
+		return DM_RESP_BAD_REQUEST;
+	}
+
+	if (pps_fname && os_file_exists(pps_fname)) {
+		ret = oma_dm_run_add(ctx, locuri, add, pps, pps_fname);
+		if (ret != DM_RESP_OK) {
+			xml_node_get_text_free(ctx->xml, locuri);
+			return ret;
+		}
+		ret = 0;
+		os_strlcpy(fname, pps_fname, sizeof(fname));
+	} else
+		ret = hs20_add_pps_mo(ctx, locuri, node, fname, sizeof(fname));
+	xml_node_get_text_free(ctx->xml, locuri);
+	if (ret < 0)
+		return ret == -2 ? DM_RESP_ALREADY_EXISTS :
+			DM_RESP_COMMAND_FAILED;
+
+	if (ctx->no_reconnect == 2) {
+		os_snprintf(ctx->pps_fname, sizeof(ctx->pps_fname), "%s",
+			    fname);
+		ctx->pps_cred_set = 1;
+		return DM_RESP_OK;
+	}
+
+	wpa_printf(MSG_INFO, "Updating wpa_supplicant credentials");
+	cmd_set_pps(ctx, fname);
+
+	if (ctx->no_reconnect)
+		return DM_RESP_OK;
+
+	wpa_printf(MSG_INFO, "Requesting reconnection with updated configuration");
+	if (wpa_command(ctx->ifname, "INTERWORKING_SELECT auto") < 0)
+		wpa_printf(MSG_INFO, "Failed to request wpa_supplicant to reconnect");
+
+	return DM_RESP_OK;
+}
+
+
+static int oma_dm_replace(struct hs20_osu_client *ctx, xml_node_t *replace,
+			  xml_node_t *pps, const char *pps_fname)
+{
+	char *locuri, *pos;
+	size_t fqdn_len;
+	xml_node_t *node, *tnds, *unode, *pps_node, *parent;
+	char *data;
+	int use_tnds = 0;
+
+	locuri = oma_dm_get_target_locuri(ctx, replace);
+	if (locuri == NULL)
+		return DM_RESP_BAD_REQUEST;
+
+	wpa_printf(MSG_INFO, "Replace command target LocURI: %s", locuri);
+	if (os_strncasecmp(locuri, "./Wi-Fi/", 8) != 0) {
+		wpa_printf(MSG_INFO, "Do not allow Replace outside ./Wi-Fi");
+		os_free(locuri);
+		return DM_RESP_PERMISSION_DENIED;
+	}
+	pos = locuri + 8;
+
+	if (ctx->fqdn == NULL) {
+		os_free(locuri);
+		return DM_RESP_COMMAND_FAILED;
+	}
+	fqdn_len = os_strlen(ctx->fqdn);
+	if (os_strncasecmp(pos, ctx->fqdn, fqdn_len) != 0 ||
+	    pos[fqdn_len] != '/') {
+		wpa_printf(MSG_INFO, "Do not allow Replace outside ./Wi-Fi/%s",
+			   ctx->fqdn);
+		os_free(locuri);
+		return DM_RESP_PERMISSION_DENIED;
+	}
+	pos += fqdn_len + 1;
+
+	if (os_strncasecmp(pos, "PerProviderSubscription/", 24) != 0) {
+		wpa_printf(MSG_INFO,
+			   "Do not allow Replace outside ./Wi-Fi/%s/PerProviderSubscription",
+			   ctx->fqdn);
+		os_free(locuri);
+		return DM_RESP_PERMISSION_DENIED;
+	}
+	pos += 24;
+
+	wpa_printf(MSG_INFO, "Replace command for PPS node %s", pos);
+
+	pps_node = get_node(ctx->xml, pps, pos);
+	if (pps_node == NULL) {
+		wpa_printf(MSG_INFO, "Specified PPS node not found");
+		os_free(locuri);
+		return DM_RESP_NOT_FOUND;
+	}
+
+	node = get_node(ctx->xml, replace, "Item/Meta/Type");
+	if (node) {
+		char *type;
+		type = xml_node_get_text(ctx->xml, node);
+		use_tnds = node &&
+			os_strstr(type, "application/vnd.syncml.dmtnds+xml");
+	}
+
+	node = get_node(ctx->xml, replace, "Item/Data");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "No Replace/Item/Data found");
+		os_free(locuri);
+		return DM_RESP_BAD_REQUEST;
+	}
+
+	data = xml_node_get_text(ctx->xml, node);
+	if (data == NULL) {
+		wpa_printf(MSG_INFO, "Could not get Replace/Item/Data text");
+		os_free(locuri);
+		return DM_RESP_BAD_REQUEST;
+	}
+
+	wpa_printf(MSG_DEBUG, "Replace/Item/Data: %s", data);
+
+	if (use_tnds) {
+		tnds = xml_node_from_buf(ctx->xml, data);
+		xml_node_get_text_free(ctx->xml, data);
+		if (tnds == NULL) {
+			wpa_printf(MSG_INFO,
+				   "Could not parse Replace/Item/Data text");
+			os_free(locuri);
+			return DM_RESP_BAD_REQUEST;
+		}
+
+		unode = tnds_to_mo(ctx->xml, tnds);
+		xml_node_free(ctx->xml, tnds);
+		if (unode == NULL) {
+			wpa_printf(MSG_INFO, "Could not parse TNDS text");
+			os_free(locuri);
+			return DM_RESP_BAD_REQUEST;
+		}
+
+		debug_dump_node(ctx, "Parsed TNDS", unode);
+
+		parent = xml_node_get_parent(ctx->xml, pps_node);
+		xml_node_detach(ctx->xml, pps_node);
+		xml_node_add_child(ctx->xml, parent, unode);
+	} else {
+		xml_node_set_text(ctx->xml, pps_node, data);
+		xml_node_get_text_free(ctx->xml, data);
+	}
+
+	os_free(locuri);
+
+	if (update_pps_file(ctx, pps_fname, pps) < 0)
+		return DM_RESP_COMMAND_FAILED;
+
+	ctx->pps_updated = 1;
+
+	return DM_RESP_OK;
+}
+
+
+static int oma_dm_get(struct hs20_osu_client *ctx, xml_node_t *get,
+		      xml_node_t *pps, const char *pps_fname, char **value)
+{
+	char *locuri, *pos;
+	size_t fqdn_len;
+	xml_node_t *pps_node;
+	const char *name;
+
+	*value = NULL;
+
+	locuri = oma_dm_get_target_locuri(ctx, get);
+	if (locuri == NULL)
+		return DM_RESP_BAD_REQUEST;
+
+	wpa_printf(MSG_INFO, "Get command target LocURI: %s", locuri);
+	if (os_strncasecmp(locuri, "./Wi-Fi/", 8) != 0) {
+		wpa_printf(MSG_INFO, "Do not allow Get outside ./Wi-Fi");
+		os_free(locuri);
+		return DM_RESP_PERMISSION_DENIED;
+	}
+	pos = locuri + 8;
+
+	if (ctx->fqdn == NULL)
+		return DM_RESP_COMMAND_FAILED;
+	fqdn_len = os_strlen(ctx->fqdn);
+	if (os_strncasecmp(pos, ctx->fqdn, fqdn_len) != 0 ||
+	    pos[fqdn_len] != '/') {
+		wpa_printf(MSG_INFO, "Do not allow Get outside ./Wi-Fi/%s",
+			   ctx->fqdn);
+		os_free(locuri);
+		return DM_RESP_PERMISSION_DENIED;
+	}
+	pos += fqdn_len + 1;
+
+	if (os_strncasecmp(pos, "PerProviderSubscription/", 24) != 0) {
+		wpa_printf(MSG_INFO,
+			   "Do not allow Get outside ./Wi-Fi/%s/PerProviderSubscription",
+			   ctx->fqdn);
+		os_free(locuri);
+		return DM_RESP_PERMISSION_DENIED;
+	}
+	pos += 24;
+
+	wpa_printf(MSG_INFO, "Get command for PPS node %s", pos);
+
+	pps_node = get_node(ctx->xml, pps, pos);
+	if (pps_node == NULL) {
+		wpa_printf(MSG_INFO, "Specified PPS node not found");
+		os_free(locuri);
+		return DM_RESP_NOT_FOUND;
+	}
+
+	name = xml_node_get_localname(ctx->xml, pps_node);
+	wpa_printf(MSG_INFO, "Get command returned node with name '%s'", name);
+	if (os_strcasecmp(name, "Password") == 0) {
+		wpa_printf(MSG_INFO, "Do not allow Get for Password node");
+		os_free(locuri);
+		return DM_RESP_PERMISSION_DENIED;
+	}
+
+	/*
+	 * TODO: No support for DMTNDS, so if interior node, reply with a
+	 * list of children node names in Results element. The child list type is
+	 * defined in [DMTND].
+	 */
+
+	*value = xml_node_get_text(ctx->xml, pps_node);
+	if (*value == NULL)
+		return DM_RESP_COMMAND_FAILED;
+
+	return DM_RESP_OK;
+}
+
+
+static int oma_dm_get_cmdid(struct hs20_osu_client *ctx, xml_node_t *node)
+{
+	xml_node_t *cnode;
+	char *str;
+	int ret;
+
+	cnode = get_node(ctx->xml, node, "CmdID");
+	if (cnode == NULL)
+		return 0;
+
+	str = xml_node_get_text(ctx->xml, cnode);
+	if (str == NULL)
+		return 0;
+	ret = atoi(str);
+	xml_node_get_text_free(ctx->xml, str);
+	return ret;
+}
+
+
+static xml_node_t * oma_dm_send_recv(struct hs20_osu_client *ctx,
+				     const char *url, xml_node_t *syncml,
+				     const char *ext_hdr, const char *ca_fname,
+				     const char *username, const char *password,
+				     const char *client_cert,
+				     const char *client_key)
+{
+	xml_node_t *resp;
+	char *str, *res;
+	char *resp_uri = NULL;
+
+	str = xml_node_to_str(ctx->xml, syncml);
+	xml_node_free(ctx->xml, syncml);
+	if (str == NULL)
+		return NULL;
+
+	wpa_printf(MSG_INFO, "Send OMA DM Package");
+	write_summary(ctx, "Send OMA DM Package");
+	os_free(ctx->server_url);
+	ctx->server_url = os_strdup(url);
+	res = http_post(ctx->http, url, str, "application/vnd.syncml.dm+xml",
+			ext_hdr, ca_fname, username, password,
+			client_cert, client_key, NULL);
+	os_free(str);
+	os_free(resp_uri);
+	resp_uri = NULL;
+
+	if (res == NULL) {
+		const char *err = http_get_err(ctx->http);
+		if (err) {
+			wpa_printf(MSG_INFO, "HTTP error: %s", err);
+			write_result(ctx, "HTTP error: %s", err);
+		} else {
+			write_summary(ctx, "Failed to send OMA DM Package");
+		}
+		return NULL;
+	}
+	wpa_printf(MSG_DEBUG, "Server response: %s", res);
+
+	wpa_printf(MSG_INFO, "Process OMA DM Package");
+	write_summary(ctx, "Process received OMA DM Package");
+	resp = xml_node_from_buf(ctx->xml, res);
+	os_free(res);
+	if (resp == NULL) {
+		wpa_printf(MSG_INFO, "Failed to parse OMA DM response");
+		return NULL;
+	}
+
+	debug_dump_node(ctx, "OMA DM Package", resp);
+
+	return resp;
+}
+
+
+static xml_node_t * oma_dm_process(struct hs20_osu_client *ctx, const char *url,
+				   xml_node_t *resp, int msgid,
+				   char **ret_resp_uri,
+				   xml_node_t *pps, const char *pps_fname)
+{
+	xml_node_t *syncml, *syncbody, *hdr, *body, *child;
+	const char *name;
+	char *resp_uri = NULL;
+	int server_msgid = 0;
+	int cmdid = 0;
+	int server_cmdid;
+	int resp_needed = 0;
+	char *tmp;
+	int final = 0;
+	char *locuri;
+
+	*ret_resp_uri = NULL;
+
+	name = xml_node_get_localname(ctx->xml, resp);
+	if (name == NULL || os_strcasecmp(name, "SyncML") != 0) {
+		wpa_printf(MSG_INFO, "SyncML node not found");
+		return NULL;
+	}
+
+	hdr = get_node(ctx->xml, resp, "SyncHdr");
+	body = get_node(ctx->xml, resp, "SyncBody");
+	if (hdr == NULL || body == NULL) {
+		wpa_printf(MSG_INFO, "Could not find SyncHdr or SyncBody");
+		return NULL;
+	}
+
+	xml_node_for_each_child(ctx->xml, child, hdr) {
+		xml_node_for_each_check(ctx->xml, child);
+		name = xml_node_get_localname(ctx->xml, child);
+		wpa_printf(MSG_INFO, "SyncHdr %s", name);
+		if (os_strcasecmp(name, "RespURI") == 0) {
+			tmp = xml_node_get_text(ctx->xml, child);
+			if (tmp)
+				resp_uri = os_strdup(tmp);
+			xml_node_get_text_free(ctx->xml, tmp);
+		} else if (os_strcasecmp(name, "MsgID") == 0) {
+			tmp = xml_node_get_text(ctx->xml, child);
+			if (tmp)
+				server_msgid = atoi(tmp);
+			xml_node_get_text_free(ctx->xml, tmp);
+		}
+	}
+
+	wpa_printf(MSG_INFO, "Server MsgID: %d", server_msgid);
+	if (resp_uri)
+		wpa_printf(MSG_INFO, "RespURI: %s", resp_uri);
+
+	syncml = oma_dm_build_hdr(ctx, resp_uri ? resp_uri : url, msgid);
+	if (syncml == NULL) {
+		os_free(resp_uri);
+		return NULL;
+	}
+
+	syncbody = xml_node_create(ctx->xml, syncml, NULL, "SyncBody");
+	cmdid++;
+	add_status(ctx, syncbody, server_msgid, 0, cmdid, "SyncHdr",
+		   DM_RESP_AUTH_ACCEPTED, NULL);
+
+	xml_node_for_each_child(ctx->xml, child, body) {
+		xml_node_for_each_check(ctx->xml, child);
+		server_cmdid = oma_dm_get_cmdid(ctx, child);
+		name = xml_node_get_localname(ctx->xml, child);
+		wpa_printf(MSG_INFO, "SyncBody CmdID=%d - %s",
+			   server_cmdid, name);
+		if (os_strcasecmp(name, "Exec") == 0) {
+			int res = oma_dm_exec(ctx, child);
+			cmdid++;
+			locuri = oma_dm_get_target_locuri(ctx, child);
+			if (locuri == NULL)
+				res = DM_RESP_BAD_REQUEST;
+			add_status(ctx, syncbody, server_msgid, server_cmdid,
+				   cmdid, name, res, locuri);
+			os_free(locuri);
+			resp_needed = 1;
+		} else if (os_strcasecmp(name, "Add") == 0) {
+			int res = oma_dm_add(ctx, child, pps, pps_fname);
+			cmdid++;
+			locuri = oma_dm_get_target_locuri(ctx, child);
+			if (locuri == NULL)
+				res = DM_RESP_BAD_REQUEST;
+			add_status(ctx, syncbody, server_msgid, server_cmdid,
+				   cmdid, name, res, locuri);
+			os_free(locuri);
+			resp_needed = 1;
+		} else if (os_strcasecmp(name, "Replace") == 0) {
+			int res;
+			res = oma_dm_replace(ctx, child, pps, pps_fname);
+			cmdid++;
+			locuri = oma_dm_get_target_locuri(ctx, child);
+			if (locuri == NULL)
+				res = DM_RESP_BAD_REQUEST;
+			add_status(ctx, syncbody, server_msgid, server_cmdid,
+				   cmdid, name, res, locuri);
+			os_free(locuri);
+			resp_needed = 1;
+		} else if (os_strcasecmp(name, "Status") == 0) {
+			/* TODO: Verify success */
+		} else if (os_strcasecmp(name, "Get") == 0) {
+			int res;
+			char *value;
+			res = oma_dm_get(ctx, child, pps, pps_fname, &value);
+			cmdid++;
+			locuri = oma_dm_get_target_locuri(ctx, child);
+			if (locuri == NULL)
+				res = DM_RESP_BAD_REQUEST;
+			add_status(ctx, syncbody, server_msgid, server_cmdid,
+				   cmdid, name, res, locuri);
+			if (res == DM_RESP_OK && value) {
+				cmdid++;
+				add_results(ctx, syncbody, server_msgid,
+					    server_cmdid, cmdid, locuri, value);
+			}
+			os_free(locuri);
+			xml_node_get_text_free(ctx->xml, value);
+			resp_needed = 1;
+#if 0 /* TODO: MUST support */
+		} else if (os_strcasecmp(name, "Delete") == 0) {
+#endif
+#if 0 /* TODO: MUST support */
+		} else if (os_strcasecmp(name, "Sequence") == 0) {
+#endif
+		} else if (os_strcasecmp(name, "Final") == 0) {
+			final = 1;
+			break;
+		} else {
+			locuri = oma_dm_get_target_locuri(ctx, child);
+			add_status(ctx, syncbody, server_msgid, server_cmdid,
+				   cmdid, name, DM_RESP_COMMAND_NOT_IMPLEMENTED,
+				   locuri);
+			os_free(locuri);
+			resp_needed = 1;
+		}
+	}
+
+	if (!final) {
+		wpa_printf(MSG_INFO, "Final node not found");
+		xml_node_free(ctx->xml, syncml);
+		os_free(resp_uri);
+		return NULL;
+	}
+
+	if (!resp_needed) {
+		wpa_printf(MSG_INFO, "Exchange completed - no response needed");
+		xml_node_free(ctx->xml, syncml);
+		os_free(resp_uri);
+		return NULL;
+	}
+
+	xml_node_create(ctx->xml, syncbody, NULL, "Final");
+
+	debug_dump_node(ctx, "OMA-DM Package 3", syncml);
+
+	*ret_resp_uri = resp_uri;
+	return syncml;
+}
+
+
+int cmd_oma_dm_prov(struct hs20_osu_client *ctx, const char *url,
+		    const char *ca_fname)
+{
+	xml_node_t *syncml, *resp;
+	char *resp_uri = NULL;
+	int msgid = 0;
+
+	if (url == NULL) {
+		wpa_printf(MSG_INFO, "Invalid prov command (missing URL)");
+		return -1;
+	}
+
+	wpa_printf(MSG_INFO, "OMA-DM credential provisioning requested");
+	write_summary(ctx, "OMA-DM credential provisioning");
+
+	msgid++;
+	syncml = build_oma_dm_1_sub_reg(ctx, url, msgid);
+	if (syncml == NULL)
+		return -1;
+
+	while (syncml) {
+		resp = oma_dm_send_recv(ctx, resp_uri ? resp_uri : url,
+					syncml, NULL, ca_fname, NULL, NULL,
+					NULL, NULL);
+		if (resp == NULL)
+			return -1;
+
+		msgid++;
+		syncml = oma_dm_process(ctx, url, resp, msgid, &resp_uri,
+					NULL, NULL);
+		xml_node_free(ctx->xml, resp);
+	}
+
+	os_free(resp_uri);
+
+	return ctx->pps_cred_set ? 0 : -1;
+}
+
+
+int cmd_oma_dm_sim_prov(struct hs20_osu_client *ctx, const char *url,
+			const char *ca_fname)
+{
+	xml_node_t *syncml, *resp;
+	char *resp_uri = NULL;
+	int msgid = 0;
+
+	if (url == NULL) {
+		wpa_printf(MSG_INFO, "Invalid prov command (missing URL)");
+		return -1;
+	}
+
+	wpa_printf(MSG_INFO, "OMA-DM SIM provisioning requested");
+	ctx->no_reconnect = 2;
+
+	wpa_printf(MSG_INFO, "Wait for IP address before starting SIM provisioning");
+	write_summary(ctx, "Wait for IP address before starting SIM provisioning");
+
+	if (wait_ip_addr(ctx->ifname, 15) < 0) {
+		wpa_printf(MSG_INFO, "Could not get IP address for WLAN - try connection anyway");
+	}
+	write_summary(ctx, "OMA-DM SIM provisioning");
+
+	msgid++;
+	syncml = build_oma_dm_1_sub_prov(ctx, url, msgid);
+	if (syncml == NULL)
+		return -1;
+
+	while (syncml) {
+		resp = oma_dm_send_recv(ctx, resp_uri ? resp_uri : url,
+					syncml, NULL, ca_fname, NULL, NULL,
+					NULL, NULL);
+		if (resp == NULL)
+			return -1;
+
+		msgid++;
+		syncml = oma_dm_process(ctx, url, resp, msgid, &resp_uri,
+					NULL, NULL);
+		xml_node_free(ctx->xml, resp);
+	}
+
+	os_free(resp_uri);
+
+	if (ctx->pps_cred_set) {
+		wpa_printf(MSG_INFO, "Updating wpa_supplicant credentials");
+		cmd_set_pps(ctx, ctx->pps_fname);
+
+		wpa_printf(MSG_INFO, "Requesting reconnection with updated configuration");
+		write_summary(ctx, "Requesting reconnection with updated configuration");
+		if (wpa_command(ctx->ifname, "INTERWORKING_SELECT auto") < 0) {
+			wpa_printf(MSG_INFO, "Failed to request wpa_supplicant to reconnect");
+			write_summary(ctx, "Failed to request wpa_supplicant to reconnect");
+			return -1;
+		}
+	}
+
+	return ctx->pps_cred_set ? 0 : -1;
+}
+
+
+void oma_dm_pol_upd(struct hs20_osu_client *ctx, const char *address,
+		    const char *pps_fname, const char *ca_fname,
+		    const char *client_cert, const char *client_key,
+		    const char *cred_username, const char *cred_password,
+		    xml_node_t *pps)
+{
+	xml_node_t *syncml, *resp;
+	char *resp_uri = NULL;
+	int msgid = 0;
+
+	wpa_printf(MSG_INFO, "OMA-DM policy update");
+	write_summary(ctx, "OMA-DM policy update");
+
+	msgid++;
+	syncml = build_oma_dm_1_pol_upd(ctx, address, msgid);
+	if (syncml == NULL)
+		return;
+
+	while (syncml) {
+		resp = oma_dm_send_recv(ctx, resp_uri ? resp_uri : address,
+					syncml, NULL, ca_fname, cred_username,
+					cred_password, client_cert, client_key);
+		if (resp == NULL)
+			return;
+
+		msgid++;
+		syncml = oma_dm_process(ctx, address, resp, msgid, &resp_uri,
+					pps, pps_fname);
+		xml_node_free(ctx->xml, resp);
+	}
+
+	os_free(resp_uri);
+
+	if (ctx->pps_updated) {
+		wpa_printf(MSG_INFO, "Update wpa_supplicant credential based on updated PPS MO");
+		write_summary(ctx, "Update wpa_supplicant credential based on updated PPS MO and request connection");
+		cmd_set_pps(ctx, pps_fname);
+		if (wpa_command(ctx->ifname, "INTERWORKING_SELECT auto") < 0) {
+			wpa_printf(MSG_INFO,
+				   "Failed to request wpa_supplicant to reconnect");
+			write_summary(ctx,
+				      "Failed to request wpa_supplicant to reconnect");
+		}
+	}
+}
+
+
+void oma_dm_sub_rem(struct hs20_osu_client *ctx, const char *address,
+		    const char *pps_fname, const char *ca_fname,
+		    const char *client_cert, const char *client_key,
+		    const char *cred_username, const char *cred_password,
+		    xml_node_t *pps)
+{
+	xml_node_t *syncml, *resp;
+	char *resp_uri = NULL;
+	int msgid = 0;
+
+	wpa_printf(MSG_INFO, "OMA-DM subscription remediation");
+	write_summary(ctx, "OMA-DM subscription remediation");
+
+	msgid++;
+	syncml = build_oma_dm_1_sub_rem(ctx, address, msgid);
+	if (syncml == NULL)
+		return;
+
+	while (syncml) {
+		resp = oma_dm_send_recv(ctx, resp_uri ? resp_uri : address,
+					syncml, NULL, ca_fname, cred_username,
+					cred_password, client_cert, client_key);
+		if (resp == NULL)
+			return;
+
+		msgid++;
+		syncml = oma_dm_process(ctx, address, resp, msgid, &resp_uri,
+					pps, pps_fname);
+		xml_node_free(ctx->xml, resp);
+	}
+
+	os_free(resp_uri);
+
+	wpa_printf(MSG_INFO, "Update wpa_supplicant credential based on updated PPS MO and request reconnection");
+	write_summary(ctx, "Update wpa_supplicant credential based on updated PPS MO and request reconnection");
+	cmd_set_pps(ctx, pps_fname);
+	if (wpa_command(ctx->ifname, "INTERWORKING_SELECT auto") < 0) {
+		wpa_printf(MSG_INFO, "Failed to request wpa_supplicant to reconnect");
+		write_summary(ctx, "Failed to request wpa_supplicant to reconnect");
+	}
+}
+
+
+void cmd_oma_dm_add(struct hs20_osu_client *ctx, const char *pps_fname,
+		    const char *add_fname)
+{
+	xml_node_t *pps, *add;
+	int res;
+
+	ctx->fqdn = os_strdup("wi-fi.org");
+
+	pps = node_from_file(ctx->xml, pps_fname);
+	if (pps == NULL) {
+		wpa_printf(MSG_INFO, "PPS file %s could not be parsed",
+			   pps_fname);
+		return;
+	}
+
+	add = node_from_file(ctx->xml, add_fname);
+	if (add == NULL) {
+		wpa_printf(MSG_INFO, "Add file %s could not be parsed",
+			   add_fname);
+		xml_node_free(ctx->xml, pps);
+		return;
+	}
+
+	res = oma_dm_add(ctx, add, pps, pps_fname);
+	wpa_printf(MSG_INFO, "oma_dm_add --> %d", res);
+
+	xml_node_free(ctx->xml, pps);
+	xml_node_free(ctx->xml, add);
+}
+
+
+void cmd_oma_dm_replace(struct hs20_osu_client *ctx, const char *pps_fname,
+			const char *replace_fname)
+{
+	xml_node_t *pps, *replace;
+	int res;
+
+	ctx->fqdn = os_strdup("wi-fi.org");
+
+	pps = node_from_file(ctx->xml, pps_fname);
+	if (pps == NULL) {
+		wpa_printf(MSG_INFO, "PPS file %s could not be parsed",
+			   pps_fname);
+		return;
+	}
+
+	replace = node_from_file(ctx->xml, replace_fname);
+	if (replace == NULL) {
+		wpa_printf(MSG_INFO, "Replace file %s could not be parsed",
+			   replace_fname);
+		xml_node_free(ctx->xml, pps);
+		return;
+	}
+
+	res = oma_dm_replace(ctx, replace, pps, pps_fname);
+	wpa_printf(MSG_INFO, "oma_dm_replace --> %d", res);
+
+	xml_node_free(ctx->xml, pps);
+	xml_node_free(ctx->xml, replace);
+}
diff --git a/hs20/client/osu_client.c b/hs20/client/osu_client.c
new file mode 100644
index 0000000..2175fc3
--- /dev/null
+++ b/hs20/client/osu_client.c
@@ -0,0 +1,3196 @@
+/*
+ * Hotspot 2.0 OSU client
+ * Copyright (c) 2012-2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+#include <time.h>
+#include <sys/stat.h>
+
+#include "common.h"
+#include "utils/browser.h"
+#include "utils/base64.h"
+#include "utils/xml-utils.h"
+#include "utils/http-utils.h"
+#include "common/wpa_ctrl.h"
+#include "common/wpa_helpers.h"
+#include "eap_common/eap_defs.h"
+#include "crypto/crypto.h"
+#include "crypto/sha256.h"
+#include "osu_client.h"
+
+
+void write_result(struct hs20_osu_client *ctx, const char *fmt, ...)
+{
+	va_list ap;
+	FILE *f;
+	char buf[500];
+
+	va_start(ap, fmt);
+	vsnprintf(buf, sizeof(buf), fmt, ap);
+	va_end(ap);
+	write_summary(ctx, "%s", buf);
+
+	if (!ctx->result_file)
+		return;
+
+	f = fopen(ctx->result_file, "w");
+	if (f == NULL)
+		return;
+
+	va_start(ap, fmt);
+	vfprintf(f, fmt, ap);
+	va_end(ap);
+	fprintf(f, "\n");
+	fclose(f);
+}
+
+
+void write_summary(struct hs20_osu_client *ctx, const char *fmt, ...)
+{
+	va_list ap;
+	FILE *f;
+
+	if (!ctx->summary_file)
+		return;
+
+	f = fopen(ctx->summary_file, "a");
+	if (f == NULL)
+		return;
+
+	va_start(ap, fmt);
+	vfprintf(f, fmt, ap);
+	va_end(ap);
+	fprintf(f, "\n");
+	fclose(f);
+}
+
+
+void debug_dump_node(struct hs20_osu_client *ctx, const char *title,
+		     xml_node_t *node)
+{
+	char *str = xml_node_to_str(ctx->xml, node);
+	wpa_printf(MSG_DEBUG, "[hs20] %s: '%s'", title, str);
+	free(str);
+}
+
+
+static int valid_fqdn(const char *fqdn)
+{
+	const char *pos;
+
+	/* TODO: could make this more complete.. */
+	if (strchr(fqdn, '.') == 0 || strlen(fqdn) > 255)
+		return 0;
+	for (pos = fqdn; *pos; pos++) {
+		if (*pos >= 'a' && *pos <= 'z')
+			continue;
+		if (*pos >= 'A' && *pos <= 'Z')
+			continue;
+		if (*pos >= '0' && *pos <= '9')
+			continue;
+		if (*pos == '-' || *pos == '.' || *pos == '_')
+			continue;
+		return 0;
+	}
+	return 1;
+}
+
+
+int osu_get_certificate(struct hs20_osu_client *ctx, xml_node_t *getcert)
+{
+	xml_node_t *node;
+	char *url, *user = NULL, *pw = NULL;
+	char *proto;
+	int ret = -1;
+
+	proto = xml_node_get_attr_value(ctx->xml, getcert,
+					"enrollmentProtocol");
+	if (!proto)
+		return -1;
+	wpa_printf(MSG_INFO, "getCertificate - enrollmentProtocol=%s", proto);
+	write_summary(ctx, "getCertificate - enrollmentProtocol=%s", proto);
+	if (os_strcasecmp(proto, "EST") != 0) {
+		wpa_printf(MSG_INFO, "Unsupported enrollmentProtocol");
+		xml_node_get_attr_value_free(ctx->xml, proto);
+		return -1;
+	}
+	xml_node_get_attr_value_free(ctx->xml, proto);
+
+	node = get_node(ctx->xml, getcert, "enrollmentServerURI");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "Could not find enrollmentServerURI node");
+		xml_node_get_attr_value_free(ctx->xml, proto);
+		return -1;
+	}
+	url = xml_node_get_text(ctx->xml, node);
+	if (url == NULL) {
+		wpa_printf(MSG_INFO, "Could not get URL text");
+		return -1;
+	}
+	wpa_printf(MSG_INFO, "enrollmentServerURI: %s", url);
+	write_summary(ctx, "enrollmentServerURI: %s", url);
+
+	node = get_node(ctx->xml, getcert, "estUserID");
+	if (node == NULL && !ctx->client_cert_present) {
+		wpa_printf(MSG_INFO, "Could not find estUserID node");
+		goto fail;
+	}
+	if (node) {
+		user = xml_node_get_text(ctx->xml, node);
+		if (user == NULL) {
+			wpa_printf(MSG_INFO, "Could not get estUserID text");
+			goto fail;
+		}
+		wpa_printf(MSG_INFO, "estUserID: %s", user);
+		write_summary(ctx, "estUserID: %s", user);
+	}
+
+	node = get_node(ctx->xml, getcert, "estPassword");
+	if (node == NULL && !ctx->client_cert_present) {
+		wpa_printf(MSG_INFO, "Could not find estPassword node");
+		goto fail;
+	}
+	if (node) {
+		pw = xml_node_get_base64_text(ctx->xml, node, NULL);
+		if (pw == NULL) {
+			wpa_printf(MSG_INFO, "Could not get estPassword text");
+			goto fail;
+		}
+		wpa_printf(MSG_INFO, "estPassword: %s", pw);
+	}
+
+	mkdir("Cert", S_IRWXU);
+	if (est_load_cacerts(ctx, url) < 0 ||
+	    est_build_csr(ctx, url) < 0 ||
+	    est_simple_enroll(ctx, url, user, pw) < 0)
+		goto fail;
+
+	ret = 0;
+fail:
+	xml_node_get_text_free(ctx->xml, url);
+	xml_node_get_text_free(ctx->xml, user);
+	xml_node_get_text_free(ctx->xml, pw);
+
+	return ret;
+}
+
+
+static int process_est_cert(struct hs20_osu_client *ctx, xml_node_t *cert,
+			    const char *fqdn)
+{
+	u8 digest1[SHA256_MAC_LEN], digest2[SHA256_MAC_LEN];
+	char *der, *pem;
+	size_t der_len, pem_len;
+	char *fingerprint;
+	char buf[200];
+
+	wpa_printf(MSG_INFO, "PPS for certificate credential - fqdn=%s", fqdn);
+
+	fingerprint = xml_node_get_text(ctx->xml, cert);
+	if (fingerprint == NULL)
+		return -1;
+	if (hexstr2bin(fingerprint, digest1, SHA256_MAC_LEN) < 0) {
+		wpa_printf(MSG_INFO, "Invalid SHA256 hash value");
+		write_result(ctx, "Invalid client certificate SHA256 hash value in PPS");
+		xml_node_get_text_free(ctx->xml, fingerprint);
+		return -1;
+	}
+	xml_node_get_text_free(ctx->xml, fingerprint);
+
+	der = os_readfile("Cert/est_cert.der", &der_len);
+	if (der == NULL) {
+		wpa_printf(MSG_INFO, "Could not find client certificate from EST");
+		write_result(ctx, "Could not find client certificate from EST");
+		return -1;
+	}
+
+	if (sha256_vector(1, (const u8 **) &der, &der_len, digest2) < 0) {
+		os_free(der);
+		return -1;
+	}
+	os_free(der);
+
+	if (os_memcmp(digest1, digest2, sizeof(digest1)) != 0) {
+		wpa_printf(MSG_INFO, "Client certificate from EST does not match fingerprint from PPS MO");
+		write_result(ctx, "Client certificate from EST does not match fingerprint from PPS MO");
+		return -1;
+	}
+
+	wpa_printf(MSG_INFO, "Client certificate from EST matches PPS MO");
+	unlink("Cert/est_cert.der");
+
+	os_snprintf(buf, sizeof(buf), "SP/%s/client-ca.pem", fqdn);
+	if (rename("Cert/est-cacerts.pem", buf) < 0) {
+		wpa_printf(MSG_INFO, "Could not move est-cacerts.pem to client-ca.pem: %s",
+			   strerror(errno));
+		return -1;
+	}
+	pem = os_readfile(buf, &pem_len);
+
+	os_snprintf(buf, sizeof(buf), "SP/%s/client-cert.pem", fqdn);
+	if (rename("Cert/est_cert.pem", buf) < 0) {
+		wpa_printf(MSG_INFO, "Could not move est_cert.pem to client-cert.pem: %s",
+			   strerror(errno));
+		os_free(pem);
+		return -1;
+	}
+
+	if (pem) {
+		FILE *f = fopen(buf, "a");
+		if (f) {
+			fwrite(pem, pem_len, 1, f);
+			fclose(f);
+		}
+		os_free(pem);
+	}
+
+	os_snprintf(buf, sizeof(buf), "SP/%s/client-key.pem", fqdn);
+	if (rename("Cert/privkey-plain.pem", buf) < 0) {
+		wpa_printf(MSG_INFO, "Could not move privkey-plain.pem to client-key.pem: %s",
+			   strerror(errno));
+		return -1;
+	}
+
+	unlink("Cert/est-req.b64");
+	unlink("Cert/est-req.pem");
+	unlink("Cert/est-resp.raw");
+	rmdir("Cert");
+
+	return 0;
+}
+
+
+#define TMP_CERT_DL_FILE "tmp-cert-download"
+
+static int download_cert(struct hs20_osu_client *ctx, xml_node_t *params,
+			 const char *fname)
+{
+	xml_node_t *url_node, *hash_node;
+	char *url, *hash;
+	char *cert;
+	size_t len;
+	u8 digest1[SHA256_MAC_LEN], digest2[SHA256_MAC_LEN];
+	int res;
+	unsigned char *b64;
+	FILE *f;
+
+	url_node = get_node(ctx->xml, params, "CertURL");
+	hash_node = get_node(ctx->xml, params, "CertSHA256Fingerprint");
+	if (url_node == NULL || hash_node == NULL)
+		return -1;
+	url = xml_node_get_text(ctx->xml, url_node);
+	hash = xml_node_get_text(ctx->xml, hash_node);
+	if (url == NULL || hash == NULL) {
+		xml_node_get_text_free(ctx->xml, url);
+		xml_node_get_text_free(ctx->xml, hash);
+		return -1;
+	}
+
+	wpa_printf(MSG_INFO, "CertURL: %s", url);
+	wpa_printf(MSG_INFO, "SHA256 hash: %s", hash);
+
+	if (hexstr2bin(hash, digest1, SHA256_MAC_LEN) < 0) {
+		wpa_printf(MSG_INFO, "Invalid SHA256 hash value");
+		write_result(ctx, "Invalid SHA256 hash value for downloaded certificate");
+		xml_node_get_text_free(ctx->xml, hash);
+		return -1;
+	}
+	xml_node_get_text_free(ctx->xml, hash);
+
+	write_summary(ctx, "Download certificate from %s", url);
+	res = http_download_file(ctx->http, url, TMP_CERT_DL_FILE, NULL);
+	xml_node_get_text_free(ctx->xml, url);
+	if (res < 0)
+		return -1;
+
+	cert = os_readfile(TMP_CERT_DL_FILE, &len);
+	remove(TMP_CERT_DL_FILE);
+	if (cert == NULL)
+		return -1;
+
+	if (sha256_vector(1, (const u8 **) &cert, &len, digest2) < 0) {
+		os_free(cert);
+		return -1;
+	}
+
+	if (os_memcmp(digest1, digest2, sizeof(digest1)) != 0) {
+		wpa_printf(MSG_INFO, "Downloaded certificate fingerprint did not match");
+		write_result(ctx, "Downloaded certificate fingerprint did not match");
+		os_free(cert);
+		return -1;
+	}
+
+	b64 = base64_encode((unsigned char *) cert, len, NULL);
+	os_free(cert);
+	if (b64 == NULL)
+		return -1;
+
+	f = fopen(fname, "wb");
+	if (f == NULL) {
+		os_free(b64);
+		return -1;
+	}
+
+	fprintf(f, "-----BEGIN CERTIFICATE-----\n"
+		"%s"
+		"-----END CERTIFICATE-----\n",
+		b64);
+
+	os_free(b64);
+	fclose(f);
+
+	wpa_printf(MSG_INFO, "Downloaded certificate into %s and validated fingerprint",
+		   fname);
+	write_summary(ctx, "Downloaded certificate into %s and validated fingerprint",
+		      fname);
+
+	return 0;
+}
+
+
+static int cmd_dl_osu_ca(struct hs20_osu_client *ctx, const char *pps_fname,
+			 const char *ca_fname)
+{
+	xml_node_t *pps, *node;
+	int ret;
+
+	pps = node_from_file(ctx->xml, pps_fname);
+	if (pps == NULL) {
+		wpa_printf(MSG_INFO, "Could not read or parse '%s'", pps_fname);
+		return -1;
+	}
+
+	node = get_child_node(ctx->xml, pps,
+			      "SubscriptionUpdate/TrustRoot");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "No SubscriptionUpdate/TrustRoot/CertURL found from PPS");
+		xml_node_free(ctx->xml, pps);
+		return -1;
+	}
+
+	ret = download_cert(ctx, node, ca_fname);
+	xml_node_free(ctx->xml, pps);
+
+	return ret;
+}
+
+
+static int cmd_dl_polupd_ca(struct hs20_osu_client *ctx, const char *pps_fname,
+			    const char *ca_fname)
+{
+	xml_node_t *pps, *node;
+	int ret;
+
+	pps = node_from_file(ctx->xml, pps_fname);
+	if (pps == NULL) {
+		wpa_printf(MSG_INFO, "Could not read or parse '%s'", pps_fname);
+		return -1;
+	}
+
+	node = get_child_node(ctx->xml, pps,
+			      "PolicyUpdate/TrustRoot");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "No PolicyUpdate/TrustRoot/CertURL found from PPS");
+		xml_node_free(ctx->xml, pps);
+		return -1;
+	}
+
+	ret = download_cert(ctx, node, ca_fname);
+	xml_node_free(ctx->xml, pps);
+
+	return ret;
+}
+
+
+static int cmd_dl_aaa_ca(struct hs20_osu_client *ctx, const char *pps_fname,
+			 const char *ca_fname)
+{
+	xml_node_t *pps, *node, *aaa;
+	int ret;
+
+	pps = node_from_file(ctx->xml, pps_fname);
+	if (pps == NULL) {
+		wpa_printf(MSG_INFO, "Could not read or parse '%s'", pps_fname);
+		return -1;
+	}
+
+	node = get_child_node(ctx->xml, pps,
+			      "AAAServerTrustRoot");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "No AAAServerTrustRoot/CertURL found from PPS");
+		xml_node_free(ctx->xml, pps);
+		return -1;
+	}
+
+	aaa = xml_node_first_child(ctx->xml, node);
+	if (aaa == NULL) {
+		wpa_printf(MSG_INFO, "No AAAServerTrustRoot/CertURL found from PPS");
+		xml_node_free(ctx->xml, pps);
+		return -1;
+	}
+
+	ret = download_cert(ctx, aaa, ca_fname);
+	xml_node_free(ctx->xml, pps);
+
+	return ret;
+}
+
+
+static int download_trust_roots(struct hs20_osu_client *ctx,
+				const char *pps_fname)
+{
+	char *dir, *pos;
+	char fname[300];
+	int ret;
+
+	dir = os_strdup(pps_fname);
+	if (dir == NULL)
+		return -1;
+	pos = os_strrchr(dir, '/');
+	if (pos == NULL) {
+		os_free(dir);
+		return -1;
+	}
+	*pos = '\0';
+
+	snprintf(fname, sizeof(fname), "%s/ca.pem", dir);
+	ret = cmd_dl_osu_ca(ctx, pps_fname, fname);
+	snprintf(fname, sizeof(fname), "%s/polupd-ca.pem", dir);
+	cmd_dl_polupd_ca(ctx, pps_fname, fname);
+	snprintf(fname, sizeof(fname), "%s/aaa-ca.pem", dir);
+	cmd_dl_aaa_ca(ctx, pps_fname, fname);
+
+	os_free(dir);
+
+	return ret;
+}
+
+
+static int server_dnsname_suffix_match(struct hs20_osu_client *ctx,
+				       const char *fqdn)
+{
+	size_t match_len, len, i;
+	const char *val;
+
+	match_len = os_strlen(fqdn);
+
+	for (i = 0; i < ctx->server_dnsname_count; i++) {
+		wpa_printf(MSG_INFO,
+			   "Checking suffix match against server dNSName %s",
+			   ctx->server_dnsname[i]);
+		val = ctx->server_dnsname[i];
+		len = os_strlen(val);
+
+		if (match_len > len)
+			continue;
+
+		if (os_strncasecmp(val + len - match_len, fqdn, match_len) != 0)
+			continue; /* no match */
+
+		if (match_len == len)
+			return 1; /* exact match */
+
+		if (val[len - match_len - 1] == '.')
+			return 1; /* full label match completes suffix match */
+
+		/* Reject due to incomplete label match */
+	}
+
+	/* None of the dNSName(s) matched */
+	return 0;
+}
+
+
+int hs20_add_pps_mo(struct hs20_osu_client *ctx, const char *uri,
+		    xml_node_t *add_mo, char *fname, size_t fname_len)
+{
+	char *str;
+	char *fqdn, *pos;
+	xml_node_t *tnds, *mo, *cert;
+	const char *name;
+	int ret;
+
+	if (strncmp(uri, "./Wi-Fi/", 8) != 0) {
+		wpa_printf(MSG_INFO, "Unsupported location for addMO to add PPS MO: '%s'",
+			   uri);
+		write_result(ctx, "Unsupported location for addMO to add PPS MO: '%s'",
+			     uri);
+		return -1;
+	}
+
+	fqdn = strdup(uri + 8);
+	if (fqdn == NULL)
+		return -1;
+	pos = strchr(fqdn, '/');
+	if (pos) {
+		if (os_strcasecmp(pos, "/PerProviderSubscription") != 0) {
+			wpa_printf(MSG_INFO, "Unsupported location for addMO to add PPS MO (extra directory): '%s'",
+				   uri);
+			write_result(ctx, "Unsupported location for addMO to "
+				     "add PPS MO (extra directory): '%s'", uri);
+			return -1;
+		}
+		*pos = '\0'; /* remove trailing slash and PPS node name */
+	}
+	wpa_printf(MSG_INFO, "SP FQDN: %s", fqdn);
+
+	if (!server_dnsname_suffix_match(ctx, fqdn)) {
+		wpa_printf(MSG_INFO, "FQDN '%s' for new PPS MO did not have suffix match with server's dNSName values",
+			   fqdn);
+		write_result(ctx, "FQDN '%s' for new PPS MO did not have suffix match with server's dNSName values",
+			     fqdn);
+		free(fqdn);
+		return -1;
+	}
+
+	if (!valid_fqdn(fqdn)) {
+		wpa_printf(MSG_INFO, "Invalid FQDN '%s'", fqdn);
+		write_result(ctx, "Invalid FQDN '%s'", fqdn);
+		free(fqdn);
+		return -1;
+	}
+
+	mkdir("SP", S_IRWXU);
+	snprintf(fname, fname_len, "SP/%s", fqdn);
+	if (mkdir(fname, S_IRWXU) < 0) {
+		if (errno != EEXIST) {
+			int err = errno;
+			wpa_printf(MSG_INFO, "mkdir(%s) failed: %s",
+				   fname, strerror(err));
+			free(fqdn);
+			return -1;
+		}
+	}
+
+	snprintf(fname, fname_len, "SP/%s/pps.xml", fqdn);
+
+	if (os_file_exists(fname)) {
+		wpa_printf(MSG_INFO, "PPS file '%s' exists - reject addMO",
+			   fname);
+		write_result(ctx, "PPS file '%s' exists - reject addMO",
+			     fname);
+		free(fqdn);
+		return -2;
+	}
+	wpa_printf(MSG_INFO, "Using PPS file: %s", fname);
+
+	str = xml_node_get_text(ctx->xml, add_mo);
+	if (str == NULL) {
+		wpa_printf(MSG_INFO, "Could not extract MO text");
+		free(fqdn);
+		return -1;
+	}
+	wpa_printf(MSG_DEBUG, "[hs20] addMO text: '%s'", str);
+
+	tnds = xml_node_from_buf(ctx->xml, str);
+	xml_node_get_text_free(ctx->xml, str);
+	if (tnds == NULL) {
+		wpa_printf(MSG_INFO, "[hs20] Could not parse addMO text");
+		free(fqdn);
+		return -1;
+	}
+
+	mo = tnds_to_mo(ctx->xml, tnds);
+	if (mo == NULL) {
+		wpa_printf(MSG_INFO, "[hs20] Could not parse addMO TNDS text");
+		free(fqdn);
+		return -1;
+	}
+
+	debug_dump_node(ctx, "Parsed TNDS", mo);
+
+	name = xml_node_get_localname(ctx->xml, mo);
+	if (os_strcasecmp(name, "PerProviderSubscription") != 0) {
+		wpa_printf(MSG_INFO, "[hs20] Unexpected PPS MO root node name '%s'",
+			   name);
+		free(fqdn);
+		return -1;
+	}
+
+	cert = get_child_node(ctx->xml, mo,
+			      "Credential/DigitalCertificate/"
+			      "CertSHA256Fingerprint");
+	if (cert && process_est_cert(ctx, cert, fqdn) < 0) {
+		xml_node_free(ctx->xml, mo);
+		free(fqdn);
+		return -1;
+	}
+	free(fqdn);
+
+	if (node_to_file(ctx->xml, fname, mo) < 0) {
+		wpa_printf(MSG_INFO, "Could not write MO to file");
+		xml_node_free(ctx->xml, mo);
+		return -1;
+	}
+	xml_node_free(ctx->xml, mo);
+
+	wpa_printf(MSG_INFO, "A new PPS MO added as '%s'", fname);
+	write_summary(ctx, "A new PPS MO added as '%s'", fname);
+
+	ret = download_trust_roots(ctx, fname);
+	if (ret < 0) {
+		wpa_printf(MSG_INFO, "Remove invalid PPS MO file");
+		write_summary(ctx, "Remove invalid PPS MO file");
+		unlink(fname);
+	}
+
+	return ret;
+}
+
+
+int update_pps_file(struct hs20_osu_client *ctx, const char *pps_fname,
+		    xml_node_t *pps)
+{
+	char *str;
+	FILE *f;
+	char backup[300];
+
+	if (ctx->client_cert_present) {
+		xml_node_t *cert;
+		cert = get_child_node(ctx->xml, pps,
+				      "Credential/DigitalCertificate/"
+				      "CertSHA256Fingerprint");
+		if (cert && os_file_exists("Cert/est_cert.der") &&
+		    process_est_cert(ctx, cert, ctx->fqdn) < 0) {
+			wpa_printf(MSG_INFO, "EST certificate update processing failed on PPS MO update");
+			return -1;
+		}
+	}
+
+	wpa_printf(MSG_INFO, "Updating PPS MO %s", pps_fname);
+
+	str = xml_node_to_str(ctx->xml, pps);
+	wpa_printf(MSG_MSGDUMP, "[hs20] Updated PPS: '%s'", str);
+
+	snprintf(backup, sizeof(backup), "%s.bak", pps_fname);
+	rename(pps_fname, backup);
+	f = fopen(pps_fname, "w");
+	if (f == NULL) {
+		wpa_printf(MSG_INFO, "Could not write PPS");
+		rename(backup, pps_fname);
+		free(str);
+		return -1;
+	}
+	fprintf(f, "%s\n", str);
+	fclose(f);
+
+	free(str);
+
+	return 0;
+}
+
+
+void get_user_pw(struct hs20_osu_client *ctx, xml_node_t *pps,
+		 const char *alt_loc, char **user, char **pw)
+{
+	xml_node_t *node;
+
+	node = get_child_node(ctx->xml, pps,
+			      "Credential/UsernamePassword/Username");
+	if (node)
+		*user = xml_node_get_text(ctx->xml, node);
+
+	node = get_child_node(ctx->xml, pps,
+			      "Credential/UsernamePassword/Password");
+	if (node)
+		*pw = xml_node_get_base64_text(ctx->xml, node, NULL);
+
+	node = get_child_node(ctx->xml, pps, alt_loc);
+	if (node) {
+		xml_node_t *a;
+		a = get_node(ctx->xml, node, "Username");
+		if (a) {
+			xml_node_get_text_free(ctx->xml, *user);
+			*user = xml_node_get_text(ctx->xml, a);
+			wpa_printf(MSG_INFO, "Use OSU username '%s'", *user);
+		}
+
+		a = get_node(ctx->xml, node, "Password");
+		if (a) {
+			free(*pw);
+			*pw = xml_node_get_base64_text(ctx->xml, a, NULL);
+			wpa_printf(MSG_INFO, "Use OSU password");
+		}
+	}
+}
+
+
+/* Remove old credentials based on HomeSP/FQDN */
+static void remove_sp_creds(struct hs20_osu_client *ctx, const char *fqdn)
+{
+	char cmd[300];
+	os_snprintf(cmd, sizeof(cmd), "REMOVE_CRED provisioning_sp=%s", fqdn);
+	if (wpa_command(ctx->ifname, cmd) < 0)
+		wpa_printf(MSG_INFO, "Failed to remove old credential(s)");
+}
+
+
+static void set_pps_cred_policy_spe(struct hs20_osu_client *ctx, int id,
+				    xml_node_t *spe)
+{
+	xml_node_t *ssid;
+	char *txt;
+
+	ssid = get_node(ctx->xml, spe, "SSID");
+	if (ssid == NULL)
+		return;
+	txt = xml_node_get_text(ctx->xml, ssid);
+	if (txt == NULL)
+		return;
+	wpa_printf(MSG_DEBUG, "- Policy/SPExclusionList/<X+>/SSID = %s", txt);
+	if (set_cred_quoted(ctx->ifname, id, "excluded_ssid", txt) < 0)
+		wpa_printf(MSG_INFO, "Failed to set cred excluded_ssid");
+	xml_node_get_text_free(ctx->xml, txt);
+}
+
+
+static void set_pps_cred_policy_spel(struct hs20_osu_client *ctx, int id,
+				     xml_node_t *spel)
+{
+	xml_node_t *child;
+
+	xml_node_for_each_child(ctx->xml, child, spel) {
+		xml_node_for_each_check(ctx->xml, child);
+		set_pps_cred_policy_spe(ctx, id, child);
+	}
+}
+
+
+static void set_pps_cred_policy_prp(struct hs20_osu_client *ctx, int id,
+				    xml_node_t *prp)
+{
+	xml_node_t *node;
+	char *txt = NULL, *pos;
+	char *prio, *country_buf = NULL;
+	const char *country;
+	char val[200];
+	int priority;
+
+	node = get_node(ctx->xml, prp, "Priority");
+	if (node == NULL)
+		return;
+	prio = xml_node_get_text(ctx->xml, node);
+	if (prio == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- Policy/PreferredRoamingPartnerList/<X+>/Priority = %s",
+		   prio);
+	priority = atoi(prio);
+	xml_node_get_text_free(ctx->xml, prio);
+
+	node = get_node(ctx->xml, prp, "Country");
+	if (node) {
+		country_buf = xml_node_get_text(ctx->xml, node);
+		if (country_buf == NULL)
+			return;
+		country = country_buf;
+		wpa_printf(MSG_INFO, "- Policy/PreferredRoamingPartnerList/<X+>/Country = %s",
+			   country);
+	} else {
+		country = "*";
+	}
+
+	node = get_node(ctx->xml, prp, "FQDN_Match");
+	if (node == NULL)
+		goto out;
+	txt = xml_node_get_text(ctx->xml, node);
+	if (txt == NULL)
+		goto out;
+	wpa_printf(MSG_INFO, "- Policy/PreferredRoamingPartnerList/<X+>/FQDN_Match = %s",
+		   txt);
+	pos = strrchr(txt, ',');
+	if (pos == NULL)
+		goto out;
+	*pos++ = '\0';
+
+	snprintf(val, sizeof(val), "%s,%d,%d,%s", txt,
+		 strcmp(pos, "includeSubdomains") != 0, priority, country);
+	if (set_cred_quoted(ctx->ifname, id, "roaming_partner", val) < 0)
+		wpa_printf(MSG_INFO, "Failed to set cred roaming_partner");
+out:
+	xml_node_get_text_free(ctx->xml, country_buf);
+	xml_node_get_text_free(ctx->xml, txt);
+}
+
+
+static void set_pps_cred_policy_prpl(struct hs20_osu_client *ctx, int id,
+				     xml_node_t *prpl)
+{
+	xml_node_t *child;
+
+	xml_node_for_each_child(ctx->xml, child, prpl) {
+		xml_node_for_each_check(ctx->xml, child);
+		set_pps_cred_policy_prp(ctx, id, child);
+	}
+}
+
+
+static void set_pps_cred_policy_min_backhaul(struct hs20_osu_client *ctx, int id,
+					     xml_node_t *min_backhaul)
+{
+	xml_node_t *node;
+	char *type, *dl = NULL, *ul = NULL;
+	int home;
+
+	node = get_node(ctx->xml, min_backhaul, "NetworkType");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "Ignore MinBackhaulThreshold without mandatory NetworkType node");
+		return;
+	}
+
+	type = xml_node_get_text(ctx->xml, node);
+	if (type == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- Policy/MinBackhaulThreshold/<X+>/NetworkType = %s",
+		   type);
+	if (os_strcasecmp(type, "home") == 0)
+		home = 1;
+	else if (os_strcasecmp(type, "roaming") == 0)
+		home = 0;
+	else {
+		wpa_printf(MSG_INFO, "Ignore MinBackhaulThreshold with invalid NetworkType");
+		xml_node_get_text_free(ctx->xml, type);
+		return;
+	}
+	xml_node_get_text_free(ctx->xml, type);
+
+	node = get_node(ctx->xml, min_backhaul, "DLBandwidth");
+	if (node)
+		dl = xml_node_get_text(ctx->xml, node);
+
+	node = get_node(ctx->xml, min_backhaul, "ULBandwidth");
+	if (node)
+		ul = xml_node_get_text(ctx->xml, node);
+
+	if (dl == NULL && ul == NULL) {
+		wpa_printf(MSG_INFO, "Ignore MinBackhaulThreshold without either DLBandwidth or ULBandwidth nodes");
+		return;
+	}
+
+	if (dl)
+		wpa_printf(MSG_INFO, "- Policy/MinBackhaulThreshold/<X+>/DLBandwidth = %s",
+			   dl);
+	if (ul)
+		wpa_printf(MSG_INFO, "- Policy/MinBackhaulThreshold/<X+>/ULBandwidth = %s",
+			   ul);
+
+	if (home) {
+		if (dl &&
+		    set_cred(ctx->ifname, id, "min_dl_bandwidth_home", dl) < 0)
+			wpa_printf(MSG_INFO, "Failed to set cred bandwidth limit");
+		if (ul &&
+		    set_cred(ctx->ifname, id, "min_ul_bandwidth_home", ul) < 0)
+			wpa_printf(MSG_INFO, "Failed to set cred bandwidth limit");
+	} else {
+		if (dl &&
+		    set_cred(ctx->ifname, id, "min_dl_bandwidth_roaming", dl) <
+		    0)
+			wpa_printf(MSG_INFO, "Failed to set cred bandwidth limit");
+		if (ul &&
+		    set_cred(ctx->ifname, id, "min_ul_bandwidth_roaming", ul) <
+		    0)
+			wpa_printf(MSG_INFO, "Failed to set cred bandwidth limit");
+	}
+
+	xml_node_get_text_free(ctx->xml, dl);
+	xml_node_get_text_free(ctx->xml, ul);
+}
+
+
+static void set_pps_cred_policy_min_backhaul_list(struct hs20_osu_client *ctx,
+						  int id, xml_node_t *node)
+{
+	xml_node_t *child;
+
+	wpa_printf(MSG_INFO, "- Policy/MinBackhaulThreshold");
+
+	xml_node_for_each_child(ctx->xml, child, node) {
+		xml_node_for_each_check(ctx->xml, child);
+		set_pps_cred_policy_min_backhaul(ctx, id, child);
+	}
+}
+
+
+static void set_pps_cred_policy_update(struct hs20_osu_client *ctx, int id,
+				       xml_node_t *node)
+{
+	wpa_printf(MSG_INFO, "- Policy/PolicyUpdate");
+	/* Not used in wpa_supplicant */
+}
+
+
+static void set_pps_cred_policy_required_proto_port(struct hs20_osu_client *ctx,
+						    int id, xml_node_t *tuple)
+{
+	xml_node_t *node;
+	char *proto, *port;
+	char *buf;
+	size_t buflen;
+
+	node = get_node(ctx->xml, tuple, "IPProtocol");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "Ignore RequiredProtoPortTuple without mandatory IPProtocol node");
+		return;
+	}
+
+	proto = xml_node_get_text(ctx->xml, node);
+	if (proto == NULL)
+		return;
+
+	wpa_printf(MSG_INFO, "- Policy/RequiredProtoPortTuple/<X+>/IPProtocol = %s",
+		   proto);
+
+	node = get_node(ctx->xml, tuple, "PortNumber");
+	port = node ? xml_node_get_text(ctx->xml, node) : NULL;
+	if (port) {
+		wpa_printf(MSG_INFO, "- Policy/RequiredProtoPortTuple/<X+>/PortNumber = %s",
+			   port);
+		buflen = os_strlen(proto) + os_strlen(port) + 10;
+		buf = os_malloc(buflen);
+		if (buf)
+			os_snprintf(buf, buflen, "%s:%s", proto, port);
+		xml_node_get_text_free(ctx->xml, port);
+	} else {
+		buflen = os_strlen(proto) + 10;
+		buf = os_malloc(buflen);
+		if (buf)
+			os_snprintf(buf, buflen, "%s", proto);
+	}
+
+	xml_node_get_text_free(ctx->xml, proto);
+
+	if (buf == NULL)
+		return;
+
+	if (set_cred(ctx->ifname, id, "req_conn_capab", buf) < 0)
+		wpa_printf(MSG_INFO, "Could not set req_conn_capab");
+
+	os_free(buf);
+}
+
+
+static void set_pps_cred_policy_required_proto_ports(struct hs20_osu_client *ctx,
+						     int id, xml_node_t *node)
+{
+	xml_node_t *child;
+
+	wpa_printf(MSG_INFO, "- Policy/RequiredProtoPortTuple");
+
+	xml_node_for_each_child(ctx->xml, child, node) {
+		xml_node_for_each_check(ctx->xml, child);
+		set_pps_cred_policy_required_proto_port(ctx, id, child);
+	}
+}
+
+
+static void set_pps_cred_policy_max_bss_load(struct hs20_osu_client *ctx, int id,
+					     xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	if (str == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- Policy/MaximumBSSLoadValue - %s", str);
+	if (set_cred(ctx->ifname, id, "max_bss_load", str) < 0)
+		wpa_printf(MSG_INFO, "Failed to set cred max_bss_load limit");
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_policy(struct hs20_osu_client *ctx, int id,
+				xml_node_t *node)
+{
+	xml_node_t *child;
+	const char *name;
+
+	wpa_printf(MSG_INFO, "- Policy");
+
+	xml_node_for_each_child(ctx->xml, child, node) {
+		xml_node_for_each_check(ctx->xml, child);
+		name = xml_node_get_localname(ctx->xml, child);
+		if (os_strcasecmp(name, "PreferredRoamingPartnerList") == 0)
+			set_pps_cred_policy_prpl(ctx, id, child);
+		else if (os_strcasecmp(name, "MinBackhaulThreshold") == 0)
+			set_pps_cred_policy_min_backhaul_list(ctx, id, child);
+		else if (os_strcasecmp(name, "PolicyUpdate") == 0)
+			set_pps_cred_policy_update(ctx, id, child);
+		else if (os_strcasecmp(name, "SPExclusionList") == 0)
+			set_pps_cred_policy_spel(ctx, id, child);
+		else if (os_strcasecmp(name, "RequiredProtoPortTuple") == 0)
+			set_pps_cred_policy_required_proto_ports(ctx, id, child);
+		else if (os_strcasecmp(name, "MaximumBSSLoadValue") == 0)
+			set_pps_cred_policy_max_bss_load(ctx, id, child);
+		else
+			wpa_printf(MSG_INFO, "Unknown Policy node '%s'", name);
+	}
+}
+
+
+static void set_pps_cred_priority(struct hs20_osu_client *ctx, int id,
+				  xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	if (str == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- CredentialPriority = %s", str);
+	if (set_cred(ctx->ifname, id, "sp_priority", str) < 0)
+		wpa_printf(MSG_INFO, "Failed to set cred sp_priority");
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_aaa_server_trust_root(struct hs20_osu_client *ctx,
+					       int id, xml_node_t *node)
+{
+	wpa_printf(MSG_INFO, "- AAAServerTrustRoot - TODO");
+}
+
+
+static void set_pps_cred_sub_update(struct hs20_osu_client *ctx, int id,
+				    xml_node_t *node)
+{
+	wpa_printf(MSG_INFO, "- SubscriptionUpdate");
+	/* not used within wpa_supplicant */
+}
+
+
+static void set_pps_cred_home_sp_network_id(struct hs20_osu_client *ctx,
+					    int id, xml_node_t *node)
+{
+	xml_node_t *ssid_node, *hessid_node;
+	char *ssid, *hessid;
+
+	ssid_node = get_node(ctx->xml, node, "SSID");
+	if (ssid_node == NULL) {
+		wpa_printf(MSG_INFO, "Ignore HomeSP/NetworkID without mandatory SSID node");
+		return;
+	}
+
+	hessid_node = get_node(ctx->xml, node, "HESSID");
+
+	ssid = xml_node_get_text(ctx->xml, ssid_node);
+	if (ssid == NULL)
+		return;
+	hessid = hessid_node ? xml_node_get_text(ctx->xml, hessid_node) : NULL;
+
+	wpa_printf(MSG_INFO, "- HomeSP/NetworkID/<X+>/SSID = %s", ssid);
+	if (hessid)
+		wpa_printf(MSG_INFO, "- HomeSP/NetworkID/<X+>/HESSID = %s",
+			   hessid);
+
+	/* TODO: Configure to wpa_supplicant */
+
+	xml_node_get_text_free(ctx->xml, ssid);
+	xml_node_get_text_free(ctx->xml, hessid);
+}
+
+
+static void set_pps_cred_home_sp_network_ids(struct hs20_osu_client *ctx,
+					     int id, xml_node_t *node)
+{
+	xml_node_t *child;
+
+	wpa_printf(MSG_INFO, "- HomeSP/NetworkID");
+
+	xml_node_for_each_child(ctx->xml, child, node) {
+		xml_node_for_each_check(ctx->xml, child);
+		set_pps_cred_home_sp_network_id(ctx, id, child);
+	}
+}
+
+
+static void set_pps_cred_home_sp_friendly_name(struct hs20_osu_client *ctx,
+					       int id, xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	if (str == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- HomeSP/FriendlyName = %s", str);
+	/* not used within wpa_supplicant(?) */
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_home_sp_icon_url(struct hs20_osu_client *ctx,
+					  int id, xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	if (str == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- HomeSP/IconURL = %s", str);
+	/* not used within wpa_supplicant */
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_home_sp_fqdn(struct hs20_osu_client *ctx, int id,
+				      xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	if (str == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- HomeSP/FQDN = %s", str);
+	if (set_cred_quoted(ctx->ifname, id, "domain", str) < 0)
+		wpa_printf(MSG_INFO, "Failed to set cred domain");
+	if (set_cred_quoted(ctx->ifname, id, "domain_suffix_match", str) < 0)
+		wpa_printf(MSG_INFO, "Failed to set cred domain_suffix_match");
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_home_sp_oi(struct hs20_osu_client *ctx, int id,
+				    xml_node_t *node)
+{
+	xml_node_t *child;
+	const char *name;
+	char *homeoi = NULL;
+	int required = 0;
+	char *str;
+
+	xml_node_for_each_child(ctx->xml, child, node) {
+		xml_node_for_each_check(ctx->xml, child);
+		name = xml_node_get_localname(ctx->xml, child);
+		if (strcasecmp(name, "HomeOI") == 0 && !homeoi) {
+			homeoi = xml_node_get_text(ctx->xml, child);
+			wpa_printf(MSG_INFO, "- HomeSP/HomeOIList/<X+>/HomeOI = %s",
+				   homeoi);
+		} else if (strcasecmp(name, "HomeOIRequired") == 0) {
+			str = xml_node_get_text(ctx->xml, child);
+			wpa_printf(MSG_INFO, "- HomeSP/HomeOIList/<X+>/HomeOIRequired = '%s'",
+				   str);
+			if (str == NULL)
+				continue;
+			required = strcasecmp(str, "true") == 0;
+			xml_node_get_text_free(ctx->xml, str);
+		} else
+			wpa_printf(MSG_INFO, "Unknown HomeOIList node '%s'",
+				   name);
+	}
+
+	if (homeoi == NULL) {
+		wpa_printf(MSG_INFO, "- HomeSP/HomeOIList/<X+> without HomeOI ignored");
+		return;
+	}
+
+	wpa_printf(MSG_INFO, "- HomeSP/HomeOIList/<X+> '%s' required=%d",
+		   homeoi, required);
+
+	if (required) {
+		if (set_cred(ctx->ifname, id, "required_roaming_consortium",
+			     homeoi) < 0)
+			wpa_printf(MSG_INFO, "Failed to set cred required_roaming_consortium");
+	} else {
+		if (set_cred_quoted(ctx->ifname, id, "roaming_consortium",
+				    homeoi) < 0)
+			wpa_printf(MSG_INFO, "Failed to set cred roaming_consortium");
+	}
+
+	xml_node_get_text_free(ctx->xml, homeoi);
+}
+
+
+static void set_pps_cred_home_sp_oi_list(struct hs20_osu_client *ctx, int id,
+					 xml_node_t *node)
+{
+	xml_node_t *child;
+
+	wpa_printf(MSG_INFO, "- HomeSP/HomeOIList");
+
+	xml_node_for_each_child(ctx->xml, child, node) {
+		xml_node_for_each_check(ctx->xml, child);
+		set_pps_cred_home_sp_oi(ctx, id, child);
+	}
+}
+
+
+static void set_pps_cred_home_sp_other_partner(struct hs20_osu_client *ctx,
+					       int id, xml_node_t *node)
+{
+	xml_node_t *child;
+	const char *name;
+	char *fqdn = NULL;
+
+	xml_node_for_each_child(ctx->xml, child, node) {
+		xml_node_for_each_check(ctx->xml, child);
+		name = xml_node_get_localname(ctx->xml, child);
+		if (os_strcasecmp(name, "FQDN") == 0 && !fqdn) {
+			fqdn = xml_node_get_text(ctx->xml, child);
+			wpa_printf(MSG_INFO, "- HomeSP/OtherHomePartners/<X+>/FQDN = %s",
+				   fqdn);
+		} else
+			wpa_printf(MSG_INFO, "Unknown OtherHomePartners node '%s'",
+				   name);
+	}
+
+	if (fqdn == NULL) {
+		wpa_printf(MSG_INFO, "- HomeSP/OtherHomePartners/<X+> without FQDN ignored");
+		return;
+	}
+
+	if (set_cred_quoted(ctx->ifname, id, "domain", fqdn) < 0)
+		wpa_printf(MSG_INFO, "Failed to set cred domain for OtherHomePartners node");
+
+	xml_node_get_text_free(ctx->xml, fqdn);
+}
+
+
+static void set_pps_cred_home_sp_other_partners(struct hs20_osu_client *ctx,
+						int id,
+						xml_node_t *node)
+{
+	xml_node_t *child;
+
+	wpa_printf(MSG_INFO, "- HomeSP/OtherHomePartners");
+
+	xml_node_for_each_child(ctx->xml, child, node) {
+		xml_node_for_each_check(ctx->xml, child);
+		set_pps_cred_home_sp_other_partner(ctx, id, child);
+	}
+}
+
+
+static void set_pps_cred_home_sp_roaming_consortium_oi(
+	struct hs20_osu_client *ctx, int id, xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	if (str == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- HomeSP/RoamingConsortiumOI = %s", str);
+	/* TODO: Set to wpa_supplicant */
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_home_sp(struct hs20_osu_client *ctx, int id,
+				 xml_node_t *node)
+{
+	xml_node_t *child;
+	const char *name;
+
+	wpa_printf(MSG_INFO, "- HomeSP");
+
+	xml_node_for_each_child(ctx->xml, child, node) {
+		xml_node_for_each_check(ctx->xml, child);
+		name = xml_node_get_localname(ctx->xml, child);
+		if (os_strcasecmp(name, "NetworkID") == 0)
+			set_pps_cred_home_sp_network_ids(ctx, id, child);
+		else if (os_strcasecmp(name, "FriendlyName") == 0)
+			set_pps_cred_home_sp_friendly_name(ctx, id, child);
+		else if (os_strcasecmp(name, "IconURL") == 0)
+			set_pps_cred_home_sp_icon_url(ctx, id, child);
+		else if (os_strcasecmp(name, "FQDN") == 0)
+			set_pps_cred_home_sp_fqdn(ctx, id, child);
+		else if (os_strcasecmp(name, "HomeOIList") == 0)
+			set_pps_cred_home_sp_oi_list(ctx, id, child);
+		else if (os_strcasecmp(name, "OtherHomePartners") == 0)
+			set_pps_cred_home_sp_other_partners(ctx, id, child);
+		else if (os_strcasecmp(name, "RoamingConsortiumOI") == 0)
+			set_pps_cred_home_sp_roaming_consortium_oi(ctx, id,
+								   child);
+		else
+			wpa_printf(MSG_INFO, "Unknown HomeSP node '%s'", name);
+	}
+}
+
+
+static void set_pps_cred_sub_params(struct hs20_osu_client *ctx, int id,
+				    xml_node_t *node)
+{
+	wpa_printf(MSG_INFO, "- SubscriptionParameters");
+	/* not used within wpa_supplicant */
+}
+
+
+static void set_pps_cred_creation_date(struct hs20_osu_client *ctx, int id,
+				       xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	if (str == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- Credential/CreationDate = %s", str);
+	/* not used within wpa_supplicant */
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_expiration_date(struct hs20_osu_client *ctx, int id,
+					 xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	if (str == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- Credential/ExpirationDate = %s", str);
+	/* not used within wpa_supplicant */
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_username(struct hs20_osu_client *ctx, int id,
+				  xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	if (str == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- Credential/UsernamePassword/Username = %s",
+		   str);
+	if (set_cred_quoted(ctx->ifname, id, "username", str) < 0)
+		wpa_printf(MSG_INFO, "Failed to set cred username");
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_password(struct hs20_osu_client *ctx, int id,
+				  xml_node_t *node)
+{
+	int len, i;
+	char *pw, *hex, *pos, *end;
+
+	pw = xml_node_get_base64_text(ctx->xml, node, &len);
+	if (pw == NULL)
+		return;
+
+	wpa_printf(MSG_INFO, "- Credential/UsernamePassword/Password = %s", pw);
+
+	hex = malloc(len * 2 + 1);
+	if (hex == NULL) {
+		free(pw);
+		return;
+	}
+	end = hex + len * 2 + 1;
+	pos = hex;
+	for (i = 0; i < len; i++) {
+		snprintf(pos, end - pos, "%02x", pw[i]);
+		pos += 2;
+	}
+	free(pw);
+
+	if (set_cred(ctx->ifname, id, "password", hex) < 0)
+		wpa_printf(MSG_INFO, "Failed to set cred password");
+	free(hex);
+}
+
+
+static void set_pps_cred_machine_managed(struct hs20_osu_client *ctx, int id,
+					 xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	if (str == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- Credential/UsernamePassword/MachineManaged = %s",
+		   str);
+	/* not used within wpa_supplicant */
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_soft_token_app(struct hs20_osu_client *ctx, int id,
+					xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	if (str == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- Credential/UsernamePassword/SoftTokenApp = %s",
+		   str);
+	/* not used within wpa_supplicant */
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_able_to_share(struct hs20_osu_client *ctx, int id,
+				       xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	if (str == NULL)
+		return;
+	wpa_printf(MSG_INFO, "- Credential/UsernamePassword/AbleToShare = %s",
+		   str);
+	/* not used within wpa_supplicant */
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_eap_method(struct hs20_osu_client *ctx, int id,
+				    xml_node_t *node)
+{
+	wpa_printf(MSG_INFO, "- Credential/UsernamePassword/EAPMethod - TODO");
+}
+
+
+static void set_pps_cred_username_password(struct hs20_osu_client *ctx, int id,
+					   xml_node_t *node)
+{
+	xml_node_t *child;
+	const char *name;
+
+	wpa_printf(MSG_INFO, "- Credential/UsernamePassword");
+
+	xml_node_for_each_child(ctx->xml, child, node) {
+		xml_node_for_each_check(ctx->xml, child);
+		name = xml_node_get_localname(ctx->xml, child);
+		if (os_strcasecmp(name, "Username") == 0)
+			set_pps_cred_username(ctx, id, child);
+		else if (os_strcasecmp(name, "Password") == 0)
+			set_pps_cred_password(ctx, id, child);
+		else if (os_strcasecmp(name, "MachineManaged") == 0)
+			set_pps_cred_machine_managed(ctx, id, child);
+		else if (os_strcasecmp(name, "SoftTokenApp") == 0)
+			set_pps_cred_soft_token_app(ctx, id, child);
+		else if (os_strcasecmp(name, "AbleToShare") == 0)
+			set_pps_cred_able_to_share(ctx, id, child);
+		else if (os_strcasecmp(name, "EAPMethod") == 0)
+			set_pps_cred_eap_method(ctx, id, child);
+		else
+			wpa_printf(MSG_INFO, "Unknown Credential/UsernamePassword node '%s'",
+				   name);
+	}
+}
+
+
+static void set_pps_cred_digital_cert(struct hs20_osu_client *ctx, int id,
+				      xml_node_t *node, const char *fqdn)
+{
+	char buf[200], dir[200];
+
+	wpa_printf(MSG_INFO, "- Credential/DigitalCertificate");
+
+	if (getcwd(dir, sizeof(dir)) == NULL)
+		return;
+
+	/* TODO: could build username from Subject of Subject AltName */
+	if (set_cred_quoted(ctx->ifname, id, "username", "cert") < 0) {
+		wpa_printf(MSG_INFO, "Failed to set username");
+	}
+
+	snprintf(buf, sizeof(buf), "%s/SP/%s/client-cert.pem", dir, fqdn);
+	if (os_file_exists(buf)) {
+		if (set_cred_quoted(ctx->ifname, id, "client_cert", buf) < 0) {
+			wpa_printf(MSG_INFO, "Failed to set client_cert");
+		}
+	}
+
+	snprintf(buf, sizeof(buf), "%s/SP/%s/client-key.pem", dir, fqdn);
+	if (os_file_exists(buf)) {
+		if (set_cred_quoted(ctx->ifname, id, "private_key", buf) < 0) {
+			wpa_printf(MSG_INFO, "Failed to set private_key");
+		}
+	}
+}
+
+
+static void set_pps_cred_realm(struct hs20_osu_client *ctx, int id,
+			       xml_node_t *node, const char *fqdn, int sim)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+	char buf[200], dir[200];
+
+	if (str == NULL)
+		return;
+
+	wpa_printf(MSG_INFO, "- Credential/Realm = %s", str);
+	if (set_cred_quoted(ctx->ifname, id, "realm", str) < 0)
+		wpa_printf(MSG_INFO, "Failed to set cred realm");
+	xml_node_get_text_free(ctx->xml, str);
+
+	if (sim)
+		return;
+
+	if (getcwd(dir, sizeof(dir)) == NULL)
+		return;
+	snprintf(buf, sizeof(buf), "%s/SP/%s/aaa-ca.pem", dir, fqdn);
+	if (os_file_exists(buf)) {
+		if (set_cred_quoted(ctx->ifname, id, "ca_cert", buf) < 0) {
+			wpa_printf(MSG_INFO, "Failed to set CA cert");
+		}
+	}
+}
+
+
+static void set_pps_cred_check_aaa_cert_status(struct hs20_osu_client *ctx,
+					       int id, xml_node_t *node)
+{
+	char *str = xml_node_get_text(ctx->xml, node);
+
+	if (str == NULL)
+		return;
+
+	wpa_printf(MSG_INFO, "- Credential/CheckAAAServerCertStatus = %s", str);
+	if (os_strcasecmp(str, "true") == 0 &&
+	    set_cred(ctx->ifname, id, "ocsp", "2") < 0)
+		wpa_printf(MSG_INFO, "Failed to set cred ocsp");
+	xml_node_get_text_free(ctx->xml, str);
+}
+
+
+static void set_pps_cred_sim(struct hs20_osu_client *ctx, int id,
+			     xml_node_t *sim, xml_node_t *realm)
+{
+	xml_node_t *node;
+	char *imsi, *eaptype, *str, buf[20];
+	int type;
+	int mnc_len = 3;
+	size_t imsi_len;
+
+	node = get_node(ctx->xml, sim, "EAPType");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "No SIM/EAPType node in credential");
+		return;
+	}
+	eaptype = xml_node_get_text(ctx->xml, node);
+	if (eaptype == NULL) {
+		wpa_printf(MSG_INFO, "Could not extract SIM/EAPType");
+		return;
+	}
+	wpa_printf(MSG_INFO, " - Credential/SIM/EAPType = %s", eaptype);
+	type = atoi(eaptype);
+	xml_node_get_text_free(ctx->xml, eaptype);
+
+	switch (type) {
+	case EAP_TYPE_SIM:
+		if (set_cred(ctx->ifname, id, "eap", "SIM") < 0)
+			wpa_printf(MSG_INFO, "Could not set eap=SIM");
+		break;
+	case EAP_TYPE_AKA:
+		if (set_cred(ctx->ifname, id, "eap", "AKA") < 0)
+			wpa_printf(MSG_INFO, "Could not set eap=SIM");
+		break;
+	case EAP_TYPE_AKA_PRIME:
+		if (set_cred(ctx->ifname, id, "eap", "AKA'") < 0)
+			wpa_printf(MSG_INFO, "Could not set eap=SIM");
+		break;
+	default:
+		wpa_printf(MSG_INFO, "Unsupported SIM/EAPType %d", type);
+		return;
+	}
+
+	node = get_node(ctx->xml, sim, "IMSI");
+	if (node == NULL) {
+		wpa_printf(MSG_INFO, "No SIM/IMSI node in credential");
+		return;
+	}
+	imsi = xml_node_get_text(ctx->xml, node);
+	if (imsi == NULL) {
+		wpa_printf(MSG_INFO, "Could not extract SIM/IMSI");
+		return;
+	}
+	wpa_printf(MSG_INFO, " - Credential/SIM/IMSI = %s", imsi);
+	imsi_len = os_strlen(imsi);
+	if (imsi_len < 7 || imsi_len + 2 > sizeof(buf)) {
+		wpa_printf(MSG_INFO, "Invalid IMSI length");
+		xml_node_get_text_free(ctx->xml, imsi);
+		return;
+	}
+
+	str = xml_node_get_text(ctx->xml, node);
+	if (str) {
+		char *pos;
+		pos = os_strstr(str, "mnc");
+		if (pos && os_strlen(pos) >= 6) {
+			if (os_strncmp(imsi + 3, pos + 3, 3) == 0)
+				mnc_len = 3;
+			else if (os_strncmp(imsi + 3, pos + 4, 2) == 0)
+				mnc_len = 2;
+		}
+		xml_node_get_text_free(ctx->xml, str);
+	}
+
+	os_memcpy(buf, imsi, 3 + mnc_len);
+	buf[3 + mnc_len] = '-';
+	os_strlcpy(buf + 3 + mnc_len + 1, imsi + 3 + mnc_len,
+		   sizeof(buf) - 3 - mnc_len - 1);
+
+	xml_node_get_text_free(ctx->xml, imsi);
+
+	if (set_cred_quoted(ctx->ifname, id, "imsi", buf) < 0)
+		wpa_printf(MSG_INFO, "Could not set IMSI");
+
+	if (set_cred_quoted(ctx->ifname, id, "milenage",
+			    "90dca4eda45b53cf0f12d7c9c3bc6a89:"
+			    "cb9cccc4b9258e6dca4760379fb82581:000000000123") <
+	    0)
+		wpa_printf(MSG_INFO, "Could not set Milenage parameters");
+}
+
+
+static void set_pps_cred_credential(struct hs20_osu_client *ctx, int id,
+				    xml_node_t *node, const char *fqdn)
+{
+	xml_node_t *child, *sim, *realm;
+	const char *name;
+
+	wpa_printf(MSG_INFO, "- Credential");
+
+	sim = get_node(ctx->xml, node, "SIM");
+	realm = get_node(ctx->xml, node, "Realm");
+
+	xml_node_for_each_child(ctx->xml, child, node) {
+		xml_node_for_each_check(ctx->xml, child);
+		name = xml_node_get_localname(ctx->xml, child);
+		if (os_strcasecmp(name, "CreationDate") == 0)
+			set_pps_cred_creation_date(ctx, id, child);
+		else if (os_strcasecmp(name, "ExpirationDate") == 0)
+			set_pps_cred_expiration_date(ctx, id, child);
+		else if (os_strcasecmp(name, "UsernamePassword") == 0)
+			set_pps_cred_username_password(ctx, id, child);
+		else if (os_strcasecmp(name, "DigitalCertificate") == 0)
+			set_pps_cred_digital_cert(ctx, id, child, fqdn);
+		else if (os_strcasecmp(name, "Realm") == 0)
+			set_pps_cred_realm(ctx, id, child, fqdn, sim != NULL);
+		else if (os_strcasecmp(name, "CheckAAAServerCertStatus") == 0)
+			set_pps_cred_check_aaa_cert_status(ctx, id, child);
+		else if (os_strcasecmp(name, "SIM") == 0)
+			set_pps_cred_sim(ctx, id, child, realm);
+		else
+			wpa_printf(MSG_INFO, "Unknown Credential node '%s'",
+				   name);
+	}
+}
+
+
+static void set_pps_credential(struct hs20_osu_client *ctx, int id,
+			       xml_node_t *cred, const char *fqdn)
+{
+	xml_node_t *child;
+	const char *name;
+
+	xml_node_for_each_child(ctx->xml, child, cred) {
+		xml_node_for_each_check(ctx->xml, child);
+		name = xml_node_get_localname(ctx->xml, child);
+		if (os_strcasecmp(name, "Policy") == 0)
+			set_pps_cred_policy(ctx, id, child);
+		else if (os_strcasecmp(name, "CredentialPriority") == 0)
+			set_pps_cred_priority(ctx, id, child);
+		else if (os_strcasecmp(name, "AAAServerTrustRoot") == 0)
+			set_pps_cred_aaa_server_trust_root(ctx, id, child);
+		else if (os_strcasecmp(name, "SubscriptionUpdate") == 0)
+			set_pps_cred_sub_update(ctx, id, child);
+		else if (os_strcasecmp(name, "HomeSP") == 0)
+			set_pps_cred_home_sp(ctx, id, child);
+		else if (os_strcasecmp(name, "SubscriptionParameters") == 0)
+			set_pps_cred_sub_params(ctx, id, child);
+		else if (os_strcasecmp(name, "Credential") == 0)
+			set_pps_cred_credential(ctx, id, child, fqdn);
+		else
+			wpa_printf(MSG_INFO, "Unknown credential node '%s'",
+				   name);
+	}
+}
+
+
+static void set_pps(struct hs20_osu_client *ctx, xml_node_t *pps,
+		    const char *fqdn)
+{
+	xml_node_t *child;
+	const char *name;
+	int id;
+	char *update_identifier = NULL;
+
+	/*
+	 * TODO: Could consider more complex mechanism that would remove
+	 * credentials only if there are changes in the information sent to
+	 * wpa_supplicant.
+	 */
+	remove_sp_creds(ctx, fqdn);
+
+	xml_node_for_each_child(ctx->xml, child, pps) {
+		xml_node_for_each_check(ctx->xml, child);
+		name = xml_node_get_localname(ctx->xml, child);
+		if (os_strcasecmp(name, "UpdateIdentifier") == 0) {
+			update_identifier = xml_node_get_text(ctx->xml, child);
+			if (update_identifier) {
+				wpa_printf(MSG_INFO, "- UpdateIdentifier = %s",
+					   update_identifier);
+				break;
+			}
+		}
+	}
+
+	xml_node_for_each_child(ctx->xml, child, pps) {
+		xml_node_for_each_check(ctx->xml, child);
+		name = xml_node_get_localname(ctx->xml, child);
+		if (os_strcasecmp(name, "UpdateIdentifier") == 0)
+			continue;
+		id = add_cred(ctx->ifname);
+		if (id < 0) {
+			wpa_printf(MSG_INFO, "Failed to add credential to wpa_supplicant");
+			write_summary(ctx, "Failed to add credential to wpa_supplicant");
+			break;
+		}
+		write_summary(ctx, "Add a credential to wpa_supplicant");
+		if (update_identifier &&
+		    set_cred(ctx->ifname, id, "update_identifier",
+			     update_identifier) < 0)
+			wpa_printf(MSG_INFO, "Failed to set update_identifier");
+		if (set_cred_quoted(ctx->ifname, id, "provisioning_sp", fqdn) <
+		    0)
+			wpa_printf(MSG_INFO, "Failed to set provisioning_sp");
+		wpa_printf(MSG_INFO, "credential localname: '%s'", name);
+		set_pps_credential(ctx, id, child, fqdn);
+		ctx->pps_cred_set = 1;
+	}
+
+	xml_node_get_text_free(ctx->xml, update_identifier);
+}
+
+
+void cmd_set_pps(struct hs20_osu_client *ctx, const char *pps_fname)
+{
+	xml_node_t *pps;
+	const char *fqdn;
+	char *fqdn_buf = NULL, *pos;
+
+	pps = node_from_file(ctx->xml, pps_fname);
+	if (pps == NULL) {
+		wpa_printf(MSG_INFO, "Could not read or parse '%s'", pps_fname);
+		return;
+	}
+
+	fqdn = os_strstr(pps_fname, "SP/");
+	if (fqdn) {
+		fqdn_buf = os_strdup(fqdn + 3);
+		if (fqdn_buf == NULL)
+			return;
+		pos = os_strchr(fqdn_buf, '/');
+		if (pos)
+			*pos = '\0';
+		fqdn = fqdn_buf;
+	} else
+		fqdn = "wi-fi.org";
+
+	wpa_printf(MSG_INFO, "Set PPS MO info to wpa_supplicant - SP FQDN %s",
+		   fqdn);
+	set_pps(ctx, pps, fqdn);
+
+	os_free(fqdn_buf);
+	xml_node_free(ctx->xml, pps);
+}
+
+
+static int cmd_get_fqdn(struct hs20_osu_client *ctx, const char *pps_fname)
+{
+	xml_node_t *pps, *node;
+	char *fqdn = NULL;
+
+	pps = node_from_file(ctx->xml, pps_fname);
+	if (pps == NULL) {
+		wpa_printf(MSG_INFO, "Could not read or parse '%s'", pps_fname);
+		return -1;
+	}
+
+	node = get_child_node(ctx->xml, pps, "HomeSP/FQDN");
+	if (node)
+		fqdn = xml_node_get_text(ctx->xml, node);
+
+	xml_node_free(ctx->xml, pps);
+
+	if (fqdn) {
+		FILE *f = fopen("pps-fqdn", "w");
+		if (f) {
+			fprintf(f, "%s", fqdn);
+			fclose(f);
+		}
+		xml_node_get_text_free(ctx->xml, fqdn);
+		return 0;
+	}
+
+	xml_node_get_text_free(ctx->xml, fqdn);
+	return -1;
+}
+
+
+static void cmd_to_tnds(struct hs20_osu_client *ctx, const char *in_fname,
+			const char *out_fname, const char *urn, int use_path)
+{
+	xml_node_t *mo, *node;
+
+	mo = node_from_file(ctx->xml, in_fname);
+	if (mo == NULL) {
+		wpa_printf(MSG_INFO, "Could not read or parse '%s'", in_fname);
+		return;
+	}
+
+	node = mo_to_tnds(ctx->xml, mo, use_path, urn, NULL);
+	if (node) {
+		node_to_file(ctx->xml, out_fname, node);
+		xml_node_free(ctx->xml, node);
+	}
+
+	xml_node_free(ctx->xml, mo);
+}
+
+
+static void cmd_from_tnds(struct hs20_osu_client *ctx, const char *in_fname,
+			  const char *out_fname)
+{
+	xml_node_t *tnds, *mo;
+
+	tnds = node_from_file(ctx->xml, in_fname);
+	if (tnds == NULL) {
+		wpa_printf(MSG_INFO, "Could not read or parse '%s'", in_fname);
+		return;
+	}
+
+	mo = tnds_to_mo(ctx->xml, tnds);
+	if (mo) {
+		node_to_file(ctx->xml, out_fname, mo);
+		xml_node_free(ctx->xml, mo);
+	}
+
+	xml_node_free(ctx->xml, tnds);
+}
+
+
+struct osu_icon {
+	int id;
+	char lang[4];
+	char mime_type[256];
+	char filename[256];
+};
+
+struct osu_data {
+	char bssid[20];
+	char url[256];
+	unsigned int methods;
+	char osu_ssid[33];
+	char osu_nai[256];
+	struct osu_lang_text friendly_name[MAX_OSU_VALS];
+	size_t friendly_name_count;
+	struct osu_lang_text serv_desc[MAX_OSU_VALS];
+	size_t serv_desc_count;
+	struct osu_icon icon[MAX_OSU_VALS];
+	size_t icon_count;
+};
+
+
+static struct osu_data * parse_osu_providers(const char *fname, size_t *count)
+{
+	FILE *f;
+	char buf[1000];
+	struct osu_data *osu = NULL, *last = NULL;
+	size_t osu_count = 0;
+	char *pos, *end;
+
+	f = fopen(fname, "r");
+	if (f == NULL) {
+		wpa_printf(MSG_ERROR, "Could not open %s", fname);
+		return NULL;
+	}
+
+	while (fgets(buf, sizeof(buf), f)) {
+		pos = strchr(buf, '\n');
+		if (pos)
+			*pos = '\0';
+
+		if (strncmp(buf, "OSU-PROVIDER ", 13) == 0) {
+			last = realloc(osu, (osu_count + 1) * sizeof(*osu));
+			if (last == NULL)
+				break;
+			osu = last;
+			last = &osu[osu_count++];
+			memset(last, 0, sizeof(*last));
+			snprintf(last->bssid, sizeof(last->bssid), "%s",
+				 buf + 13);
+			continue;
+		}
+		if (!last)
+			continue;
+
+		if (strncmp(buf, "uri=", 4) == 0) {
+			snprintf(last->url, sizeof(last->url), "%s", buf + 4);
+			continue;
+		}
+
+		if (strncmp(buf, "methods=", 8) == 0) {
+			last->methods = strtol(buf + 8, NULL, 16);
+			continue;
+		}
+
+		if (strncmp(buf, "osu_ssid=", 9) == 0) {
+			snprintf(last->osu_ssid, sizeof(last->osu_ssid),
+				 "%s", buf + 9);
+			continue;
+		}
+
+		if (os_strncmp(buf, "osu_nai=", 8) == 0) {
+			os_snprintf(last->osu_nai, sizeof(last->osu_nai),
+				    "%s", buf + 8);
+			continue;
+		}
+
+		if (strncmp(buf, "friendly_name=", 14) == 0) {
+			struct osu_lang_text *txt;
+			if (last->friendly_name_count == MAX_OSU_VALS)
+				continue;
+			pos = strchr(buf + 14, ':');
+			if (pos == NULL)
+				continue;
+			*pos++ = '\0';
+			txt = &last->friendly_name[last->friendly_name_count++];
+			snprintf(txt->lang, sizeof(txt->lang), "%s", buf + 14);
+			snprintf(txt->text, sizeof(txt->text), "%s", pos);
+		}
+
+		if (strncmp(buf, "desc=", 5) == 0) {
+			struct osu_lang_text *txt;
+			if (last->serv_desc_count == MAX_OSU_VALS)
+				continue;
+			pos = strchr(buf + 5, ':');
+			if (pos == NULL)
+				continue;
+			*pos++ = '\0';
+			txt = &last->serv_desc[last->serv_desc_count++];
+			snprintf(txt->lang, sizeof(txt->lang), "%s", buf + 5);
+			snprintf(txt->text, sizeof(txt->text), "%s", pos);
+		}
+
+		if (strncmp(buf, "icon=", 5) == 0) {
+			struct osu_icon *icon;
+			if (last->icon_count == MAX_OSU_VALS)
+				continue;
+			icon = &last->icon[last->icon_count++];
+			icon->id = atoi(buf + 5);
+			pos = strchr(buf, ':');
+			if (pos == NULL)
+				continue;
+			pos = strchr(pos + 1, ':');
+			if (pos == NULL)
+				continue;
+			pos = strchr(pos + 1, ':');
+			if (pos == NULL)
+				continue;
+			pos++;
+			end = strchr(pos, ':');
+			if (!end)
+				continue;
+			*end = '\0';
+			snprintf(icon->lang, sizeof(icon->lang), "%s", pos);
+			pos = end + 1;
+
+			end = strchr(pos, ':');
+			if (end)
+				*end = '\0';
+			snprintf(icon->mime_type, sizeof(icon->mime_type),
+				 "%s", pos);
+			if (!pos)
+				continue;
+			pos = end + 1;
+
+			end = strchr(pos, ':');
+			if (end)
+				*end = '\0';
+			snprintf(icon->filename, sizeof(icon->filename),
+				 "%s", pos);
+			continue;
+		}
+	}
+
+	fclose(f);
+
+	*count = osu_count;
+	return osu;
+}
+
+
+static int osu_connect(struct hs20_osu_client *ctx, const char *bssid,
+		       const char *ssid, const char *url, const char *ca_fname,
+		       unsigned int methods, int no_prod_assoc,
+		       const char *osu_nai)
+{
+	int id;
+	const char *ifname = ctx->ifname;
+	char buf[200];
+	struct wpa_ctrl *mon;
+	int res;
+
+	id = add_network(ifname);
+	if (id < 0)
+		return -1;
+	if (set_network_quoted(ifname, id, "ssid", ssid) < 0)
+		return -1;
+	if (osu_nai && os_strlen(osu_nai) > 0) {
+		char dir[255], fname[300];
+		if (getcwd(dir, sizeof(dir)) == NULL)
+			return -1;
+		os_snprintf(fname, sizeof(fname), "%s/osu-ca.pem", dir);
+
+		if (set_network(ifname, id, "proto", "OSEN") < 0 ||
+		    set_network(ifname, id, "key_mgmt", "OSEN") < 0 ||
+		    set_network(ifname, id, "pairwise", "CCMP") < 0 ||
+		    set_network(ifname, id, "group", "GTK_NOT_USED") < 0 ||
+		    set_network(ifname, id, "eap", "WFA-UNAUTH-TLS") < 0 ||
+		    set_network(ifname, id, "ocsp", "2") < 0 ||
+		    set_network_quoted(ifname, id, "identity", osu_nai) < 0 ||
+		    set_network_quoted(ifname, id, "ca_cert", fname) < 0)
+			return -1;
+	} else {
+		if (set_network(ifname, id, "key_mgmt", "NONE") < 0)
+			return -1;
+	}
+
+	mon = open_wpa_mon(ifname);
+	if (mon == NULL)
+		return -1;
+
+	wpa_printf(MSG_INFO, "Associate with OSU SSID");
+	write_summary(ctx, "Associate with OSU SSID");
+	snprintf(buf, sizeof(buf), "SELECT_NETWORK %d", id);
+	if (wpa_command(ifname, buf) < 0)
+		return -1;
+
+	res = get_wpa_cli_event(mon, "CTRL-EVENT-CONNECTED",
+				buf, sizeof(buf));
+
+	wpa_ctrl_detach(mon);
+	wpa_ctrl_close(mon);
+
+	if (res < 0) {
+		wpa_printf(MSG_INFO, "Could not connect");
+		write_summary(ctx, "Could not connect to OSU network");
+		wpa_printf(MSG_INFO, "Remove OSU network connection");
+		snprintf(buf, sizeof(buf), "REMOVE_NETWORK %d", id);
+		wpa_command(ifname, buf);
+		return -1;
+	}
+
+	write_summary(ctx, "Waiting for IP address for subscription registration");
+	if (wait_ip_addr(ifname, 15) < 0) {
+		wpa_printf(MSG_INFO, "Could not get IP address for WLAN - try connection anyway");
+	}
+
+	if (no_prod_assoc) {
+		if (res < 0)
+			return -1;
+		wpa_printf(MSG_INFO, "No production connection used for testing purposes");
+		write_summary(ctx, "No production connection used for testing purposes");
+		return 0;
+	}
+
+	ctx->no_reconnect = 1;
+	if (methods & 0x02)
+		res = cmd_prov(ctx, url, ca_fname);
+	else if (methods & 0x01)
+		res = cmd_oma_dm_prov(ctx, url, ca_fname);
+
+	wpa_printf(MSG_INFO, "Remove OSU network connection");
+	write_summary(ctx, "Remove OSU network connection");
+	snprintf(buf, sizeof(buf), "REMOVE_NETWORK %d", id);
+	wpa_command(ifname, buf);
+
+	if (res < 0)
+		return -1;
+
+	wpa_printf(MSG_INFO, "Requesting reconnection with updated configuration");
+	write_summary(ctx, "Requesting reconnection with updated configuration");
+	if (wpa_command(ctx->ifname, "INTERWORKING_SELECT auto") < 0) {
+		wpa_printf(MSG_INFO, "Failed to request wpa_supplicant to reconnect");
+		write_summary(ctx, "Failed to request wpa_supplicant to reconnect");
+		return -1;
+	}
+
+	return 0;
+}
+
+
+static int cmd_osu_select(struct hs20_osu_client *ctx, const char *dir,
+			  int connect, const char *ca_fname, int no_prod_assoc,
+			  const char *friendly_name)
+{
+	char fname[255];
+	FILE *f;
+	struct osu_data *osu = NULL, *last = NULL;
+	size_t osu_count, i, j;
+	int ret;
+
+	write_summary(ctx, "OSU provider selection");
+
+	if (dir == NULL) {
+		wpa_printf(MSG_INFO, "Missing dir parameter to osu_select");
+		return -1;
+	}
+
+	snprintf(fname, sizeof(fname), "%s/osu-providers.txt", dir);
+	osu = parse_osu_providers(fname, &osu_count);
+	if (osu == NULL) {
+		wpa_printf(MSG_INFO, "Could not any OSU providers from %s",
+			   fname);
+		write_result(ctx, "No OSU providers available");
+		return -1;
+	}
+
+	if (friendly_name) {
+		for (i = 0; i < osu_count; i++) {
+			last = &osu[i];
+			for (j = 0; j < last->friendly_name_count; j++) {
+				if (os_strcmp(last->friendly_name[j].text,
+					      friendly_name) == 0)
+					break;
+			}
+			if (j < last->friendly_name_count)
+				break;
+		}
+		if (i == osu_count) {
+			wpa_printf(MSG_INFO, "Requested operator friendly name '%s' not found in the list of available providers",
+				   friendly_name);
+			write_summary(ctx, "Requested operator friendly name '%s' not found in the list of available providers",
+				      friendly_name);
+			free(osu);
+			return -1;
+		}
+
+		wpa_printf(MSG_INFO, "OSU Provider selected based on requested operator friendly name '%s'",
+			   friendly_name);
+		write_summary(ctx, "OSU Provider selected based on requested operator friendly name '%s'",
+			      friendly_name);
+		ret = i + 1;
+		goto selected;
+	}
+
+	snprintf(fname, sizeof(fname), "%s/osu-providers.html", dir);
+	f = fopen(fname, "w");
+	if (f == NULL) {
+		wpa_printf(MSG_INFO, "Could not open %s", fname);
+		free(osu);
+		return -1;
+	}
+
+	fprintf(f, "<html><head>"
+		"<meta http-equiv=\"Content-type\" content=\"text/html; "
+		"charset=utf-8\"<title>Select service operator</title>"
+		"</head><body><h1>Select service operator</h1>\n");
+
+	if (osu_count == 0)
+		fprintf(f, "No online signup available\n");
+
+	for (i = 0; i < osu_count; i++) {
+		last = &osu[i];
+#ifdef ANDROID
+		fprintf(f, "<p>\n"
+			"<a href=\"http://localhost:12345/osu/%d\">"
+			"<table><tr><td>", (int) i + 1);
+#else /* ANDROID */
+		fprintf(f, "<p>\n"
+			"<a href=\"osu://%d\">"
+			"<table><tr><td>", (int) i + 1);
+#endif /* ANDROID */
+		for (j = 0; j < last->icon_count; j++) {
+			fprintf(f, "<img src=\"osu-icon-%d.%s\">\n",
+				last->icon[j].id,
+				strcasecmp(last->icon[j].mime_type,
+					   "image/png") == 0 ? "png" : "icon");
+		}
+		fprintf(f, "<td>");
+		for (j = 0; j < last->friendly_name_count; j++) {
+			fprintf(f, "<small>[%s]</small> %s<br>\n",
+				last->friendly_name[j].lang,
+				last->friendly_name[j].text);
+		}
+		fprintf(f, "<tr><td colspan=2>");
+		for (j = 0; j < last->serv_desc_count; j++) {
+			fprintf(f, "<small>[%s]</small> %s<br>\n",
+				last->serv_desc[j].lang,
+				last->serv_desc[j].text);
+		}
+		fprintf(f, "</table></a><br><small>BSSID: %s<br>\n"
+			"SSID: %s<br>\n",
+			last->bssid, last->osu_ssid);
+		if (last->osu_nai)
+			fprintf(f, "NAI: %s<br>\n", last->osu_nai);
+		fprintf(f, "URL: %s<br>\n"
+			"methods:%s%s<br>\n"
+			"</small></p>\n",
+			last->url,
+			last->methods & 0x01 ? " OMA-DM" : "",
+			last->methods & 0x02 ? " SOAP-XML-SPP" : "");
+	}
+
+	fprintf(f, "</body></html>\n");
+
+	fclose(f);
+
+	snprintf(fname, sizeof(fname), "file://%s/osu-providers.html", dir);
+	write_summary(ctx, "Start web browser with OSU provider selection page");
+	ret = hs20_web_browser(fname);
+
+selected:
+	if (ret > 0 && (size_t) ret <= osu_count) {
+		char *data;
+		size_t data_len;
+
+		wpa_printf(MSG_INFO, "Selected OSU id=%d", ret);
+		last = &osu[ret - 1];
+		ret = 0;
+		wpa_printf(MSG_INFO, "BSSID: %s", last->bssid);
+		wpa_printf(MSG_INFO, "SSID: %s", last->osu_ssid);
+		wpa_printf(MSG_INFO, "URL: %s", last->url);
+		write_summary(ctx, "Selected OSU provider id=%d BSSID=%s SSID=%s URL=%s",
+			      ret, last->bssid, last->osu_ssid, last->url);
+
+		ctx->friendly_name_count = last->friendly_name_count;
+		for (j = 0; j < last->friendly_name_count; j++) {
+			wpa_printf(MSG_INFO, "FRIENDLY_NAME: [%s]%s",
+				   last->friendly_name[j].lang,
+				   last->friendly_name[j].text);
+			os_strlcpy(ctx->friendly_name[j].lang,
+				   last->friendly_name[j].lang,
+				   sizeof(ctx->friendly_name[j].lang));
+			os_strlcpy(ctx->friendly_name[j].text,
+				   last->friendly_name[j].text,
+				   sizeof(ctx->friendly_name[j].text));
+		}
+
+		ctx->icon_count = last->icon_count;
+		for (j = 0; j < last->icon_count; j++) {
+			char fname[256];
+
+			os_snprintf(fname, sizeof(fname), "%s/osu-icon-%d.%s",
+				    dir, last->icon[j].id,
+				    strcasecmp(last->icon[j].mime_type,
+					       "image/png") == 0 ?
+				    "png" : "icon");
+			wpa_printf(MSG_INFO, "ICON: %s (%s)",
+				   fname, last->icon[j].filename);
+			os_strlcpy(ctx->icon_filename[j],
+				   last->icon[j].filename,
+				   sizeof(ctx->icon_filename[j]));
+
+			data = os_readfile(fname, &data_len);
+			if (data) {
+				sha256_vector(1, (const u8 **) &data, &data_len,
+					      ctx->icon_hash[j]);
+				os_free(data);
+			}
+		}
+
+		if (connect == 2) {
+			if (last->methods & 0x02)
+				ret = cmd_prov(ctx, last->url, ca_fname);
+			else if (last->methods & 0x01)
+				ret = cmd_oma_dm_prov(ctx, last->url, ca_fname);
+			else
+				ret = -1;
+		} else if (connect)
+			ret = osu_connect(ctx, last->bssid, last->osu_ssid,
+					  last->url, ca_fname, last->methods,
+					  no_prod_assoc, last->osu_nai);
+	} else
+		ret = -1;
+
+	free(osu);
+
+	return ret;
+}
+
+
+static int cmd_signup(struct hs20_osu_client *ctx, const char *ca_fname,
+		      int no_prod_assoc, const char *friendly_name)
+{
+	char dir[255];
+	char fname[300], buf[400];
+	struct wpa_ctrl *mon;
+	const char *ifname;
+	int res;
+
+	ifname = ctx->ifname;
+
+	if (getcwd(dir, sizeof(dir)) == NULL)
+		return -1;
+
+	snprintf(fname, sizeof(fname), "%s/osu-info", dir);
+	if (mkdir(fname, S_IRWXU | S_IRWXG) < 0 && errno != EEXIST) {
+		wpa_printf(MSG_INFO, "mkdir(%s) failed: %s",
+			   fname, strerror(errno));
+		return -1;
+	}
+
+	snprintf(buf, sizeof(buf), "SET osu_dir %s", fname);
+	if (wpa_command(ifname, buf) < 0) {
+		wpa_printf(MSG_INFO, "Failed to configure osu_dir to wpa_supplicant");
+		return -1;
+	}
+
+	mon = open_wpa_mon(ifname);
+	if (mon == NULL)
+		return -1;
+
+	wpa_printf(MSG_INFO, "Starting OSU fetch");
+	write_summary(ctx, "Starting OSU provider information fetch");
+	if (wpa_command(ifname, "FETCH_OSU") < 0) {
+		wpa_printf(MSG_INFO, "Could not start OSU fetch");
+		wpa_ctrl_detach(mon);
+		wpa_ctrl_close(mon);
+		return -1;
+	}
+	res = get_wpa_cli_event(mon, "OSU provider fetch completed",
+				buf, sizeof(buf));
+
+	wpa_ctrl_detach(mon);
+	wpa_ctrl_close(mon);
+
+	if (res < 0) {
+		wpa_printf(MSG_INFO, "OSU fetch did not complete");
+		write_summary(ctx, "OSU fetch did not complete");
+		return -1;
+	}
+	wpa_printf(MSG_INFO, "OSU provider fetch completed");
+
+	return cmd_osu_select(ctx, fname, 1, ca_fname, no_prod_assoc,
+			      friendly_name);
+}
+
+
+static void cmd_sub_rem(struct hs20_osu_client *ctx, const char *address,
+			const char *pps_fname, const char *ca_fname)
+{
+	xml_node_t *pps, *node;
+	char pps_fname_buf[300];
+	char ca_fname_buf[200];
+	char *cred_username = NULL;
+	char *cred_password = NULL;
+	char *sub_rem_uri = NULL;
+	char client_cert_buf[200];
+	char *client_cert = NULL;
+	char client_key_buf[200];
+	char *client_key = NULL;
+	int spp;
+
+	ctx->ca_fname = ca_fname;
+
+	wpa_printf(MSG_INFO, "Subscription remediation requested with Server URL: %s",
+		   address);
+
+	if (!pps_fname) {
+		char buf[256];
+		wpa_printf(MSG_INFO, "Determining PPS file based on Home SP information");
+		if (os_strncmp(address, "fqdn=", 5) == 0) {
+			wpa_printf(MSG_INFO, "Use requested FQDN from command line");
+			os_snprintf(buf, sizeof(buf), "%s", address + 5);
+			address = NULL;
+		} else if (get_wpa_status(ctx->ifname, "provisioning_sp", buf,
+					  sizeof(buf)) < 0) {
+			wpa_printf(MSG_INFO, "Could not get provisioning Home SP FQDN from wpa_supplicant");
+			return;
+		}
+		os_free(ctx->fqdn);
+		ctx->fqdn = os_strdup(buf);
+		if (ctx->fqdn == NULL)
+			return;
+		wpa_printf(MSG_INFO, "Home SP FQDN for current credential: %s",
+			   buf);
+		os_snprintf(pps_fname_buf, sizeof(pps_fname_buf),
+			    "SP/%s/pps.xml", ctx->fqdn);
+		pps_fname = pps_fname_buf;
+
+		os_snprintf(ca_fname_buf, sizeof(ca_fname_buf), "SP/%s/ca.pem",
+			    ctx->fqdn);
+		ca_fname = ca_fname_buf;
+	}
+
+	if (!os_file_exists(pps_fname)) {
+		wpa_printf(MSG_INFO, "PPS file '%s' does not exist or is not accessible",
+			   pps_fname);
+		return;
+	}
+	wpa_printf(MSG_INFO, "Using PPS file: %s", pps_fname);
+
+	if (ca_fname && !os_file_exists(ca_fname)) {
+		wpa_printf(MSG_INFO, "CA file '%s' does not exist or is not accessible",
+			   ca_fname);
+		return;
+	}
+	wpa_printf(MSG_INFO, "Using server trust root: %s", ca_fname);
+
+	pps = node_from_file(ctx->xml, pps_fname);
+	if (pps == NULL) {
+		wpa_printf(MSG_INFO, "Could not read PPS MO");
+		return;
+	}
+
+	if (!ctx->fqdn) {
+		char *tmp;
+		node = get_child_node(ctx->xml, pps, "HomeSP/FQDN");
+		if (node == NULL) {
+			wpa_printf(MSG_INFO, "No HomeSP/FQDN found from PPS");
+			return;
+		}
+		tmp = xml_node_get_text(ctx->xml, node);
+		if (tmp == NULL) {
+			wpa_printf(MSG_INFO, "No HomeSP/FQDN text found from PPS");
+			return;
+		}
+		ctx->fqdn = os_strdup(tmp);
+		xml_node_get_text_free(ctx->xml, tmp);
+		if (!ctx->fqdn) {
+			wpa_printf(MSG_INFO, "No FQDN known");
+			return;
+		}
+	}
+
+	node = get_child_node(ctx->xml, pps,
+			      "SubscriptionUpdate/UpdateMethod");
+	if (node) {
+		char *tmp;
+		tmp = xml_node_get_text(ctx->xml, node);
+		if (tmp && os_strcasecmp(tmp, "OMA-DM-ClientInitiated") == 0)
+			spp = 0;
+		else
+			spp = 1;
+	} else {
+		wpa_printf(MSG_INFO, "No UpdateMethod specified - assume SPP");
+		spp = 1;
+	}
+
+	get_user_pw(ctx, pps, "SubscriptionUpdate/UsernamePassword",
+		    &cred_username, &cred_password);
+	if (cred_username)
+		wpa_printf(MSG_INFO, "Using username: %s", cred_username);
+	if (cred_password)
+		wpa_printf(MSG_DEBUG, "Using password: %s", cred_password);
+
+	if (cred_username == NULL && cred_password == NULL &&
+	    get_child_node(ctx->xml, pps, "Credential/DigitalCertificate")) {
+		wpa_printf(MSG_INFO, "Using client certificate");
+		os_snprintf(client_cert_buf, sizeof(client_cert_buf),
+			    "SP/%s/client-cert.pem", ctx->fqdn);
+		client_cert = client_cert_buf;
+		os_snprintf(client_key_buf, sizeof(client_key_buf),
+			    "SP/%s/client-key.pem", ctx->fqdn);
+		client_key = client_key_buf;
+		ctx->client_cert_present = 1;
+	}
+
+	node = get_child_node(ctx->xml, pps, "SubscriptionUpdate/URI");
+	if (node) {
+		sub_rem_uri = xml_node_get_text(ctx->xml, node);
+		if (sub_rem_uri &&
+		    (!address || os_strcmp(address, sub_rem_uri) != 0)) {
+			wpa_printf(MSG_INFO, "Override sub rem URI based on PPS: %s",
+				   sub_rem_uri);
+			address = sub_rem_uri;
+		}
+	}
+	if (!address) {
+		wpa_printf(MSG_INFO, "Server URL not known");
+		return;
+	}
+
+	write_summary(ctx, "Wait for IP address for subscriptiom remediation");
+	wpa_printf(MSG_INFO, "Wait for IP address before starting subscription remediation");
+
+	if (wait_ip_addr(ctx->ifname, 15) < 0) {
+		wpa_printf(MSG_INFO, "Could not get IP address for WLAN - try connection anyway");
+	}
+
+	if (spp)
+		spp_sub_rem(ctx, address, pps_fname, ca_fname,
+			    client_cert, client_key,
+			    cred_username, cred_password, pps);
+	else
+		oma_dm_sub_rem(ctx, address, pps_fname, ca_fname,
+			       client_cert, client_key,
+			       cred_username, cred_password, pps);
+
+	xml_node_get_text_free(ctx->xml, sub_rem_uri);
+	xml_node_get_text_free(ctx->xml, cred_username);
+	os_free(cred_password);
+	xml_node_free(ctx->xml, pps);
+}
+
+
+static int cmd_pol_upd(struct hs20_osu_client *ctx, const char *address,
+		       const char *pps_fname, const char *ca_fname)
+{
+	xml_node_t *pps;
+	xml_node_t *node;
+	char pps_fname_buf[300];
+	char ca_fname_buf[200];
+	char *uri = NULL;
+	char *cred_username = NULL;
+	char *cred_password = NULL;
+	char client_cert_buf[200];
+	char *client_cert = NULL;
+	char client_key_buf[200];
+	char *client_key = NULL;
+	int spp;
+
+	wpa_printf(MSG_INFO, "Policy update requested");
+
+	if (!pps_fname) {
+		char buf[256];
+		wpa_printf(MSG_INFO, "Determining PPS file based on Home SP information");
+		if (os_strncmp(address, "fqdn=", 5) == 0) {
+			wpa_printf(MSG_INFO, "Use requested FQDN from command line");
+			os_snprintf(buf, sizeof(buf), "%s", address + 5);
+			address = NULL;
+		} else if (get_wpa_status(ctx->ifname, "provisioning_sp", buf,
+					  sizeof(buf)) < 0) {
+			wpa_printf(MSG_INFO, "Could not get provisioning Home SP FQDN from wpa_supplicant");
+			return -1;
+		}
+		os_free(ctx->fqdn);
+		ctx->fqdn = os_strdup(buf);
+		if (ctx->fqdn == NULL)
+			return -1;
+		wpa_printf(MSG_INFO, "Home SP FQDN for current credential: %s",
+			   buf);
+		os_snprintf(pps_fname_buf, sizeof(pps_fname_buf),
+			    "SP/%s/pps.xml", ctx->fqdn);
+		pps_fname = pps_fname_buf;
+
+		os_snprintf(ca_fname_buf, sizeof(ca_fname_buf), "SP/%s/ca.pem",
+			    buf);
+		ca_fname = ca_fname_buf;
+	}
+
+	if (!os_file_exists(pps_fname)) {
+		wpa_printf(MSG_INFO, "PPS file '%s' does not exist or is not accessible",
+			   pps_fname);
+		return -1;
+	}
+	wpa_printf(MSG_INFO, "Using PPS file: %s", pps_fname);
+
+	if (ca_fname && !os_file_exists(ca_fname)) {
+		wpa_printf(MSG_INFO, "CA file '%s' does not exist or is not accessible",
+			   ca_fname);
+		return -1;
+	}
+	wpa_printf(MSG_INFO, "Using server trust root: %s", ca_fname);
+
+	pps = node_from_file(ctx->xml, pps_fname);
+	if (pps == NULL) {
+		wpa_printf(MSG_INFO, "Could not read PPS MO");
+		return -1;
+	}
+
+	if (!ctx->fqdn) {
+		char *tmp;
+		node = get_child_node(ctx->xml, pps, "HomeSP/FQDN");
+		if (node == NULL) {
+			wpa_printf(MSG_INFO, "No HomeSP/FQDN found from PPS");
+			return -1;
+		}
+		tmp = xml_node_get_text(ctx->xml, node);
+		if (tmp == NULL) {
+			wpa_printf(MSG_INFO, "No HomeSP/FQDN text found from PPS");
+			return -1;
+		}
+		ctx->fqdn = os_strdup(tmp);
+		xml_node_get_text_free(ctx->xml, tmp);
+		if (!ctx->fqdn) {
+			wpa_printf(MSG_INFO, "No FQDN known");
+			return -1;
+		}
+	}
+
+	node = get_child_node(ctx->xml, pps,
+			      "Policy/PolicyUpdate/UpdateMethod");
+	if (node) {
+		char *tmp;
+		tmp = xml_node_get_text(ctx->xml, node);
+		if (tmp && os_strcasecmp(tmp, "OMA-DM-ClientInitiated") == 0)
+			spp = 0;
+		else
+			spp = 1;
+	} else {
+		wpa_printf(MSG_INFO, "No UpdateMethod specified - assume SPP");
+		spp = 1;
+	}
+
+	get_user_pw(ctx, pps, "Policy/PolicyUpdate/UsernamePassword",
+		    &cred_username, &cred_password);
+	if (cred_username)
+		wpa_printf(MSG_INFO, "Using username: %s", cred_username);
+	if (cred_password)
+		wpa_printf(MSG_DEBUG, "Using password: %s", cred_password);
+
+	if (cred_username == NULL && cred_password == NULL &&
+	    get_child_node(ctx->xml, pps, "Credential/DigitalCertificate")) {
+		wpa_printf(MSG_INFO, "Using client certificate");
+		os_snprintf(client_cert_buf, sizeof(client_cert_buf),
+			    "SP/%s/client-cert.pem", ctx->fqdn);
+		client_cert = client_cert_buf;
+		os_snprintf(client_key_buf, sizeof(client_key_buf),
+			    "SP/%s/client-key.pem", ctx->fqdn);
+		client_key = client_key_buf;
+	}
+
+	if (!address) {
+		node = get_child_node(ctx->xml, pps, "Policy/PolicyUpdate/URI");
+		if (node) {
+			uri = xml_node_get_text(ctx->xml, node);
+			wpa_printf(MSG_INFO, "URI based on PPS: %s", uri);
+			address = uri;
+		}
+	}
+	if (!address) {
+		wpa_printf(MSG_INFO, "Server URL not known");
+		return -1;
+	}
+
+	if (spp)
+		spp_pol_upd(ctx, address, pps_fname, ca_fname,
+			    client_cert, client_key,
+			    cred_username, cred_password, pps);
+	else
+		oma_dm_pol_upd(ctx, address, pps_fname, ca_fname,
+			       client_cert, client_key,
+			       cred_username, cred_password, pps);
+
+	xml_node_get_text_free(ctx->xml, uri);
+	xml_node_get_text_free(ctx->xml, cred_username);
+	os_free(cred_password);
+	xml_node_free(ctx->xml, pps);
+
+	return 0;
+}
+
+
+static char * get_hostname(const char *url)
+{
+	const char *pos, *end, *end2;
+	char *ret;
+
+	if (url == NULL)
+		return NULL;
+
+	pos = os_strchr(url, '/');
+	if (pos == NULL)
+		return NULL;
+	pos++;
+	if (*pos != '/')
+		return NULL;
+	pos++;
+
+	end = os_strchr(pos, '/');
+	end2 = os_strchr(pos, ':');
+	if (end && end2 && end2 < end)
+		end = end2;
+	if (end)
+		end--;
+	else {
+		end = pos;
+		while (*end)
+			end++;
+		if (end > pos)
+			end--;
+	}
+
+	ret = os_malloc(end - pos + 2);
+	if (ret == NULL)
+		return NULL;
+
+	os_memcpy(ret, pos, end - pos + 1);
+	ret[end - pos + 1] = '\0';
+
+	return ret;
+}
+
+
+static int osu_cert_cb(void *_ctx, struct http_cert *cert)
+{
+	struct hs20_osu_client *ctx = _ctx;
+	unsigned int i, j;
+	int found;
+	char *host = NULL;
+
+	wpa_printf(MSG_INFO, "osu_cert_cb");
+
+	host = get_hostname(ctx->server_url);
+
+	for (i = 0; i < ctx->server_dnsname_count; i++)
+		os_free(ctx->server_dnsname[i]);
+	os_free(ctx->server_dnsname);
+	ctx->server_dnsname = os_calloc(cert->num_dnsname, sizeof(char *));
+	ctx->server_dnsname_count = 0;
+
+	found = 0;
+	for (i = 0; i < cert->num_dnsname; i++) {
+		if (ctx->server_dnsname) {
+			ctx->server_dnsname[ctx->server_dnsname_count] =
+				os_strdup(cert->dnsname[i]);
+			if (ctx->server_dnsname[ctx->server_dnsname_count])
+				ctx->server_dnsname_count++;
+		}
+		if (host && os_strcasecmp(host, cert->dnsname[i]) == 0)
+			found = 1;
+		wpa_printf(MSG_INFO, "dNSName '%s'", cert->dnsname[i]);
+	}
+
+	if (host && !found) {
+		wpa_printf(MSG_INFO, "Server name from URL (%s) did not match any dNSName - abort connection",
+			   host);
+		write_result(ctx, "Server name from URL (%s) did not match any dNSName - abort connection",
+			     host);
+		os_free(host);
+		return -1;
+	}
+
+	os_free(host);
+
+	for (i = 0; i < cert->num_othername; i++) {
+		if (os_strcmp(cert->othername[i].oid,
+			      "1.3.6.1.4.1.40808.1.1.1") == 0) {
+			wpa_hexdump_ascii(MSG_INFO,
+					  "id-wfa-hotspot-friendlyName",
+					  cert->othername[i].data,
+					  cert->othername[i].len);
+		}
+	}
+
+	for (j = 0; j < ctx->friendly_name_count; j++) {
+		int found = 0;
+		for (i = 0; i < cert->num_othername; i++) {
+			if (os_strcmp(cert->othername[i].oid,
+				      "1.3.6.1.4.1.40808.1.1.1") != 0)
+				continue;
+			if (cert->othername[i].len < 3)
+				continue;
+			if (os_strncasecmp((char *) cert->othername[i].data,
+					   ctx->friendly_name[j].lang, 3) != 0)
+				continue;
+			if (os_strncmp((char *) cert->othername[i].data + 3,
+				       ctx->friendly_name[j].text,
+				       cert->othername[i].len - 3) == 0) {
+				found = 1;
+				break;
+			}
+		}
+
+		if (!found) {
+			wpa_printf(MSG_INFO, "No friendly name match found for '[%s]%s'",
+				   ctx->friendly_name[j].lang,
+				   ctx->friendly_name[j].text);
+			write_result(ctx, "No friendly name match found for '[%s]%s'",
+				     ctx->friendly_name[j].lang,
+				     ctx->friendly_name[j].text);
+			return -1;
+		}
+	}
+
+	for (i = 0; i < cert->num_logo; i++) {
+		struct http_logo *logo = &cert->logo[i];
+
+		wpa_printf(MSG_INFO, "logo hash alg %s uri '%s'",
+			   logo->alg_oid, logo->uri);
+		wpa_hexdump_ascii(MSG_INFO, "hashValue",
+				  logo->hash, logo->hash_len);
+	}
+
+	for (j = 0; j < ctx->icon_count; j++) {
+		int found = 0;
+		char *name = ctx->icon_filename[j];
+		size_t name_len = os_strlen(name);
+
+		wpa_printf(MSG_INFO, "Looking for icon file name '%s' match",
+			   name);
+		for (i = 0; i < cert->num_logo; i++) {
+			struct http_logo *logo = &cert->logo[i];
+			size_t uri_len = os_strlen(logo->uri);
+			char *pos;
+
+			wpa_printf(MSG_INFO, "Comparing to '%s' uri_len=%d name_len=%d",
+				   logo->uri, (int) uri_len, (int) name_len);
+			if (uri_len < 1 + name_len)
+				continue;
+			pos = &logo->uri[uri_len - name_len - 1];
+			if (*pos != '/')
+				continue;
+			pos++;
+			if (os_strcmp(pos, name) == 0) {
+				found = 1;
+				break;
+			}
+		}
+
+		if (!found) {
+			wpa_printf(MSG_INFO, "No icon filename match found for '%s'",
+				   name);
+			write_result(ctx,
+				     "No icon filename match found for '%s'",
+				     name);
+			return -1;
+		}
+	}
+
+	for (j = 0; j < ctx->icon_count; j++) {
+		int found = 0;
+
+		for (i = 0; i < cert->num_logo; i++) {
+			struct http_logo *logo = &cert->logo[i];
+
+			if (logo->hash_len != 32)
+				continue;
+			if (os_memcmp(logo->hash, ctx->icon_hash[j], 32) == 0) {
+				found = 1;
+				break;
+			}
+		}
+
+		if (!found) {
+			wpa_printf(MSG_INFO, "No icon hash match found");
+			write_result(ctx, "No icon hash match found");
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+
+static int init_ctx(struct hs20_osu_client *ctx)
+{
+	xml_node_t *devinfo, *devid;
+
+	os_memset(ctx, 0, sizeof(*ctx));
+	ctx->ifname = "wlan0";
+	ctx->xml = xml_node_init_ctx(ctx, NULL);
+	if (ctx->xml == NULL)
+		return -1;
+
+	devinfo = node_from_file(ctx->xml, "devinfo.xml");
+	if (!devinfo) {
+		wpa_printf(MSG_ERROR, "devinfo.xml not found");
+		return -1;
+	}
+
+	devid = get_node(ctx->xml, devinfo, "DevId");
+	if (devid) {
+		char *tmp = xml_node_get_text(ctx->xml, devid);
+		if (tmp) {
+			ctx->devid = os_strdup(tmp);
+			xml_node_get_text_free(ctx->xml, tmp);
+		}
+	}
+	xml_node_free(ctx->xml, devinfo);
+
+	if (ctx->devid == NULL) {
+		wpa_printf(MSG_ERROR, "Could not fetch DevId from devinfo.xml");
+		return -1;
+	}
+
+	ctx->http = http_init_ctx(ctx, ctx->xml);
+	if (ctx->http == NULL) {
+		xml_node_deinit_ctx(ctx->xml);
+		return -1;
+	}
+	http_ocsp_set(ctx->http, 2);
+	http_set_cert_cb(ctx->http, osu_cert_cb, ctx);
+
+	return 0;
+}
+
+
+static void deinit_ctx(struct hs20_osu_client *ctx)
+{
+	size_t i;
+
+	http_deinit_ctx(ctx->http);
+	xml_node_deinit_ctx(ctx->xml);
+	os_free(ctx->fqdn);
+	os_free(ctx->server_url);
+	os_free(ctx->devid);
+
+	for (i = 0; i < ctx->server_dnsname_count; i++)
+		os_free(ctx->server_dnsname[i]);
+	os_free(ctx->server_dnsname);
+}
+
+
+static void check_workarounds(struct hs20_osu_client *ctx)
+{
+	FILE *f;
+	char buf[100];
+	unsigned long int val = 0;
+
+	f = fopen("hs20-osu-client.workarounds", "r");
+	if (f == NULL)
+		return;
+
+	if (fgets(buf, sizeof(buf), f))
+		val = strtoul(buf, NULL, 16);
+
+	fclose(f);
+
+	if (val) {
+		wpa_printf(MSG_INFO, "Workarounds enabled: 0x%lx", val);
+		ctx->workarounds = val;
+		if (ctx->workarounds & WORKAROUND_OCSP_OPTIONAL)
+			http_ocsp_set(ctx->http, 1);
+	}
+}
+
+
+static void usage(void)
+{
+	printf("usage: hs20-osu-client [-dddqqKt] [-S<station ifname>] \\\n"
+	       "    [-w<wpa_supplicant ctrl_iface dir>] "
+	       "[-r<result file>] [-f<debug file>] \\\n"
+	       "    [-s<summary file>] \\\n"
+	       "    <command> [arguments..]\n"
+	       "commands:\n"
+	       "- to_tnds <XML MO> <XML MO in TNDS format> [URN]\n"
+	       "- to_tnds2 <XML MO> <XML MO in TNDS format (Path) "
+	       "[URN]>\n"
+	       "- from_tnds <XML MO in TNDS format> <XML MO>\n"
+	       "- set_pps <PerProviderSubscription XML file name>\n"
+	       "- get_fqdn <PerProviderSubscription XML file name>\n"
+	       "- pol_upd [Server URL] [PPS] [CA cert]\n"
+	       "- sub_rem <Server URL> [PPS] [CA cert]\n"
+	       "- prov <Server URL> [CA cert]\n"
+	       "- oma_dm_prov <Server URL> [CA cert]\n"
+	       "- sim_prov <Server URL> [CA cert]\n"
+	       "- oma_dm_sim_prov <Server URL> [CA cert]\n"
+	       "- signup [CA cert]\n"
+	       "- dl_osu_ca <PPS> <CA file>\n"
+	       "- dl_polupd_ca <PPS> <CA file>\n"
+	       "- dl_aaa_ca <PPS> <CA file>\n"
+	       "- browser <URL>\n"
+	       "- parse_cert <X.509 certificate (DER)>\n"
+	       "- osu_select <OSU info directory> [CA cert]\n");
+}
+
+
+int main(int argc, char *argv[])
+{
+	struct hs20_osu_client ctx;
+	int c;
+	int ret = 0;
+	int no_prod_assoc = 0;
+	const char *friendly_name = NULL;
+	const char *wpa_debug_file_path = NULL;
+	extern char *wpas_ctrl_path;
+	extern int wpa_debug_level;
+	extern int wpa_debug_show_keys;
+	extern int wpa_debug_timestamp;
+
+	if (init_ctx(&ctx) < 0)
+		return -1;
+
+	for (;;) {
+		c = getopt(argc, argv, "df:hi:KNO:qr:s:S:tw:");
+		if (c < 0)
+			break;
+		switch (c) {
+		case 'd':
+			if (wpa_debug_level > 0)
+				wpa_debug_level--;
+			break;
+		case 'f':
+			wpa_debug_file_path = optarg;
+			break;
+		case 'K':
+			wpa_debug_show_keys++;
+			break;
+		case 'N':
+			no_prod_assoc = 1;
+			break;
+		case 'O':
+			friendly_name = optarg;
+			break;
+		case 'q':
+			wpa_debug_level++;
+			break;
+		case 'r':
+			ctx.result_file = optarg;
+			break;
+		case 's':
+			ctx.summary_file = optarg;
+			break;
+		case 'S':
+			ctx.ifname = optarg;
+			break;
+		case 't':
+			wpa_debug_timestamp++;
+			break;
+		case 'w':
+			wpas_ctrl_path = optarg;
+			break;
+		case 'h':
+		default:
+			usage();
+			exit(0);
+			break;
+		}
+	}
+
+	if (argc - optind < 1) {
+		usage();
+		exit(0);
+	}
+
+	wpa_debug_open_file(wpa_debug_file_path);
+
+#ifdef __linux__
+	setlinebuf(stdout);
+#endif /* __linux__ */
+
+	if (ctx.result_file)
+		unlink(ctx.result_file);
+	wpa_printf(MSG_DEBUG, "===[hs20-osu-client START - command: %s ]======"
+		   "================", argv[optind]);
+	check_workarounds(&ctx);
+
+	if (strcmp(argv[optind], "to_tnds") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_to_tnds(&ctx, argv[optind + 1], argv[optind + 2],
+			    argc > optind + 3 ? argv[optind + 3] : NULL,
+			    0);
+	} else if (strcmp(argv[optind], "to_tnds2") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_to_tnds(&ctx, argv[optind + 1], argv[optind + 2],
+			    argc > optind + 3 ? argv[optind + 3] : NULL,
+			    1);
+	} else if (strcmp(argv[optind], "from_tnds") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_from_tnds(&ctx, argv[optind + 1], argv[optind + 2]);
+	} else if (strcmp(argv[optind], "sub_rem") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		if (argc - optind < 2)
+			wpa_printf(MSG_ERROR, "Server URL missing from command line");
+		else
+			cmd_sub_rem(&ctx, argv[optind + 1],
+				    argc > optind + 2 ? argv[optind + 2] : NULL,
+				    argc > optind + 3 ? argv[optind + 3] :
+				    NULL);
+	} else if (strcmp(argv[optind], "pol_upd") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		ret = cmd_pol_upd(&ctx, argc > 2 ? argv[optind + 1] : NULL,
+				  argc > optind + 2 ? argv[optind + 2] : NULL,
+				  argc > optind + 3 ? argv[optind + 3] : NULL);
+	} else if (strcmp(argv[optind], "prov") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_prov(&ctx, argv[optind + 1], argv[optind + 2]);
+	} else if (strcmp(argv[optind], "sim_prov") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_sim_prov(&ctx, argv[optind + 1], argv[optind + 2]);
+	} else if (strcmp(argv[optind], "dl_osu_ca") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_dl_osu_ca(&ctx, argv[optind + 1], argv[optind + 2]);
+	} else if (strcmp(argv[optind], "dl_polupd_ca") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_dl_polupd_ca(&ctx, argv[optind + 1], argv[optind + 2]);
+	} else if (strcmp(argv[optind], "dl_aaa_ca") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_dl_aaa_ca(&ctx, argv[optind + 1], argv[optind + 2]);
+	} else if (strcmp(argv[optind], "osu_select") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_osu_select(&ctx, argv[optind + 1], 2,
+			       argc > optind + 2 ? argv[optind + 2] : NULL,
+			       1, NULL);
+	} else if (strcmp(argv[optind], "signup") == 0) {
+		ret = cmd_signup(&ctx,
+				 argc > optind + 1 ? argv[optind + 1] : NULL,
+				 no_prod_assoc, friendly_name);
+	} else if (strcmp(argv[optind], "set_pps") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_set_pps(&ctx, argv[optind + 1]);
+	} else if (strcmp(argv[optind], "get_fqdn") == 0) {
+		if (argc - optind < 1) {
+			usage();
+			exit(0);
+		}
+		ret = cmd_get_fqdn(&ctx, argv[optind + 1]);
+	} else if (strcmp(argv[optind], "oma_dm_prov") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_oma_dm_prov(&ctx, argv[optind + 1], argv[optind + 2]);
+	} else if (strcmp(argv[optind], "oma_dm_sim_prov") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		if (cmd_oma_dm_sim_prov(&ctx, argv[optind + 1],
+					argv[optind + 2]) < 0) {
+			write_summary(&ctx, "Failed to complete OMA DM SIM provisioning");
+			return -1;
+		}
+	} else if (strcmp(argv[optind], "oma_dm_add") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_oma_dm_add(&ctx, argv[optind + 1], argv[optind + 2]);
+	} else if (strcmp(argv[optind], "oma_dm_replace") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		cmd_oma_dm_replace(&ctx, argv[optind + 1], argv[optind + 2]);
+	} else if (strcmp(argv[optind], "est_csr") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+		mkdir("Cert", S_IRWXU);
+		est_build_csr(&ctx, argv[optind + 1]);
+	} else if (strcmp(argv[optind], "browser") == 0) {
+		int ret;
+
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+
+		wpa_printf(MSG_INFO, "Launch web browser to URL %s",
+			   argv[optind + 1]);
+		ret = hs20_web_browser(argv[optind + 1]);
+		wpa_printf(MSG_INFO, "Web browser result: %d", ret);
+	} else if (strcmp(argv[optind], "parse_cert") == 0) {
+		if (argc - optind < 2) {
+			usage();
+			exit(0);
+		}
+
+		wpa_debug_level = MSG_MSGDUMP;
+		http_parse_x509_certificate(ctx.http, argv[optind + 1]);
+		wpa_debug_level = MSG_INFO;
+	} else {
+		wpa_printf(MSG_INFO, "Unknown command '%s'", argv[optind]);
+	}
+
+	wpa_printf(MSG_DEBUG,
+		   "===[hs20-osu-client END ]======================");
+
+	wpa_debug_close_file();
+	deinit_ctx(&ctx);
+
+	return ret;
+}
diff --git a/hs20/client/osu_client.h b/hs20/client/osu_client.h
new file mode 100644
index 0000000..ef568b4
--- /dev/null
+++ b/hs20/client/osu_client.h
@@ -0,0 +1,121 @@
+/*
+ * Hotspot 2.0 - OSU client
+ * Copyright (c) 2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#ifndef OSU_CLIENT_H
+#define OSU_CLIENT_H
+
+#define SPP_NS_URI "http://www.wi-fi.org/specifications/hotspot2dot0/v1.0/spp"
+
+#define URN_OMA_DM_DEVINFO "urn:oma:mo:oma-dm-devinfo:1.0"
+#define URN_OMA_DM_DEVDETAIL "urn:oma:mo:oma-dm-devdetail:1.0"
+#define URN_HS20_DEVDETAIL_EXT "urn:wfa:mo-ext:hotspot2dot0-devdetail-ext:1.0"
+#define URN_HS20_PPS "urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0"
+
+
+#define MAX_OSU_VALS 10
+
+struct osu_lang_text {
+	char lang[4];
+	char text[253];
+};
+
+struct hs20_osu_client {
+	struct xml_node_ctx *xml;
+	struct http_ctx *http;
+	int no_reconnect;
+	char pps_fname[300];
+	char *devid;
+	const char *result_file;
+	const char *summary_file;
+	const char *ifname;
+	const char *ca_fname;
+	char *fqdn;
+	char *server_url;
+	struct osu_lang_text friendly_name[MAX_OSU_VALS];
+	size_t friendly_name_count;
+	size_t icon_count;
+	char icon_filename[MAX_OSU_VALS][256];
+	u8 icon_hash[MAX_OSU_VALS][32];
+	int pps_cred_set;
+	int pps_updated;
+	int client_cert_present;
+	char **server_dnsname;
+	size_t server_dnsname_count;
+#define WORKAROUND_OCSP_OPTIONAL 0x00000001
+	unsigned long int workarounds;
+};
+
+
+/* osu_client.c */
+
+void write_result(struct hs20_osu_client *ctx, const char *fmt, ...)
+	__attribute__ ((format (printf, 2, 3)));
+void write_summary(struct hs20_osu_client *ctx, const char *fmt, ...)
+	__attribute__ ((format (printf, 2, 3)));
+
+void debug_dump_node(struct hs20_osu_client *ctx, const char *title,
+		     xml_node_t *node);
+int osu_get_certificate(struct hs20_osu_client *ctx, xml_node_t *getcert);
+int hs20_add_pps_mo(struct hs20_osu_client *ctx, const char *uri,
+		    xml_node_t *add_mo, char *fname, size_t fname_len);
+void get_user_pw(struct hs20_osu_client *ctx, xml_node_t *pps,
+		 const char *alt_loc, char **user, char **pw);
+int update_pps_file(struct hs20_osu_client *ctx, const char *pps_fname,
+		    xml_node_t *pps);
+void cmd_set_pps(struct hs20_osu_client *ctx, const char *pps_fname);
+
+
+/* spp_client.c */
+
+void spp_sub_rem(struct hs20_osu_client *ctx, const char *address,
+		 const char *pps_fname, const char *ca_fname,
+		 const char *client_cert, const char *client_key,
+		 const char *cred_username, const char *cred_password,
+		 xml_node_t *pps);
+void spp_pol_upd(struct hs20_osu_client *ctx, const char *address,
+		 const char *pps_fname, const char *ca_fname,
+		 const char *client_cert, const char *client_key,
+		 const char *cred_username, const char *cred_password,
+		 xml_node_t *pps);
+int cmd_prov(struct hs20_osu_client *ctx, const char *url,
+	     const char *ca_fname);
+int cmd_sim_prov(struct hs20_osu_client *ctx, const char *url,
+		 const char *ca_fname);
+
+
+/* oma_dm_client.c */
+
+int cmd_oma_dm_prov(struct hs20_osu_client *ctx, const char *url,
+		    const char *ca_fname);
+int cmd_oma_dm_sim_prov(struct hs20_osu_client *ctx, const char *url,
+			const char *ca_fname);
+void oma_dm_sub_rem(struct hs20_osu_client *ctx, const char *address,
+		    const char *pps_fname, const char *ca_fname,
+		    const char *client_cert, const char *client_key,
+		    const char *cred_username, const char *cred_password,
+		    xml_node_t *pps);
+void oma_dm_pol_upd(struct hs20_osu_client *ctx, const char *address,
+		    const char *pps_fname, const char *ca_fname,
+		    const char *client_cert, const char *client_key,
+		    const char *cred_username, const char *cred_password,
+		    xml_node_t *pps);
+void cmd_oma_dm_sub_rem(struct hs20_osu_client *ctx, const char *address,
+			const char *pps_fname, const char *ca_fname);
+void cmd_oma_dm_add(struct hs20_osu_client *ctx, const char *pps_fname,
+		    const char *add_fname);
+void cmd_oma_dm_replace(struct hs20_osu_client *ctx, const char *pps_fname,
+			const char *replace_fname);
+
+/* est.c */
+
+int est_load_cacerts(struct hs20_osu_client *ctx, const char *url);
+int est_build_csr(struct hs20_osu_client *ctx, const char *url);
+int est_simple_enroll(struct hs20_osu_client *ctx, const char *url,
+		      const char *user, const char *pw);
+
+#endif /* OSU_CLIENT_H */
diff --git a/hs20/client/spp_client.c b/hs20/client/spp_client.c
new file mode 100644
index 0000000..8022ccb
--- /dev/null
+++ b/hs20/client/spp_client.c
@@ -0,0 +1,1001 @@
+/*
+ * Hotspot 2.0 SPP client
+ * Copyright (c) 2012-2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+#include <sys/stat.h>
+
+#include "common.h"
+#include "browser.h"
+#include "wpa_ctrl.h"
+#include "wpa_helpers.h"
+#include "xml-utils.h"
+#include "http-utils.h"
+#include "utils/base64.h"
+#include "crypto/crypto.h"
+#include "crypto/sha256.h"
+#include "osu_client.h"
+
+
+static int hs20_spp_update_response(struct hs20_osu_client *ctx,
+				    const char *session_id,
+				    const char *spp_status,
+				    const char *error_code);
+static void hs20_policy_update_complete(
+	struct hs20_osu_client *ctx, const char *pps_fname);
+
+
+static char * get_spp_attr_value(struct xml_node_ctx *ctx, xml_node_t *node,
+				 char *attr_name)
+{
+	return xml_node_get_attr_value_ns(ctx, node, SPP_NS_URI, attr_name);
+}
+
+
+static int hs20_spp_validate(struct hs20_osu_client *ctx, xml_node_t *node,
+			     const char *expected_name)
+{
+	struct xml_node_ctx *xctx = ctx->xml;
+	const char *name;
+	char *err;
+	int ret;
+
+	if (!xml_node_is_element(xctx, node))
+		return -1;
+
+	name = xml_node_get_localname(xctx, node);
+	if (name == NULL)
+		return -1;
+
+	if (strcmp(expected_name, name) != 0) {
+		wpa_printf(MSG_INFO, "Unexpected SOAP method name '%s' (expected '%s')",
+			   name, expected_name);
+		write_summary(ctx, "Unexpected SOAP method name '%s' (expected '%s')",
+			      name, expected_name);
+		return -1;
+	}
+
+	ret = xml_validate(xctx, node, "spp.xsd", &err);
+	if (ret < 0) {
+		wpa_printf(MSG_INFO, "XML schema validation error(s)\n%s", err);
+		write_summary(ctx, "SPP XML schema validation failed");
+		os_free(err);
+	}
+	return ret;
+}
+
+
+static void add_mo_container(struct xml_node_ctx *ctx, xml_namespace_t *ns,
+			     xml_node_t *parent, const char *urn,
+			     const char *fname)
+{
+	xml_node_t *node;
+	xml_node_t *fnode, *tnds;
+	char *str;
+
+	fnode = node_from_file(ctx, fname);
+	if (!fnode)
+		return;
+	tnds = mo_to_tnds(ctx, fnode, 0, urn, "syncml:dmddf1.2");
+	xml_node_free(ctx, fnode);
+	if (!tnds)
+		return;
+
+	str = xml_node_to_str(ctx, tnds);
+	xml_node_free(ctx, tnds);
+	if (str == NULL)
+		return;
+
+	node = xml_node_create_text(ctx, parent, ns, "moContainer", str);
+	if (node)
+		xml_node_add_attr(ctx, node, ns, "moURN", urn);
+	os_free(str);
+}
+
+
+static xml_node_t * build_spp_post_dev_data(struct hs20_osu_client *ctx,
+					    xml_namespace_t **ret_ns,
+					    const char *session_id,
+					    const char *reason)
+{
+	xml_namespace_t *ns;
+	xml_node_t *spp_node;
+
+	write_summary(ctx, "Building sppPostDevData requestReason='%s'",
+		      reason);
+	spp_node = xml_node_create_root(ctx->xml, SPP_NS_URI, "spp", &ns,
+					"sppPostDevData");
+	if (spp_node == NULL)
+		return NULL;
+	if (ret_ns)
+		*ret_ns = ns;
+
+	xml_node_add_attr(ctx->xml, spp_node, ns, "sppVersion", "1.0");
+	xml_node_add_attr(ctx->xml, spp_node, NULL, "requestReason", reason);
+	if (session_id)
+		xml_node_add_attr(ctx->xml, spp_node, ns, "sessionID",
+				  session_id);
+	xml_node_add_attr(ctx->xml, spp_node, NULL, "redirectURI",
+			  "http://localhost:12345/");
+
+	xml_node_create_text(ctx->xml, spp_node, ns, "supportedSPPVersions",
+			     "1.0");
+	xml_node_create_text(ctx->xml, spp_node, ns, "supportedMOList",
+			     URN_HS20_PPS " " URN_OMA_DM_DEVINFO " "
+			     URN_OMA_DM_DEVDETAIL " " URN_HS20_DEVDETAIL_EXT);
+
+	add_mo_container(ctx->xml, ns, spp_node, URN_OMA_DM_DEVINFO,
+			 "devinfo.xml");
+	add_mo_container(ctx->xml, ns, spp_node, URN_OMA_DM_DEVDETAIL,
+			 "devdetail.xml");
+
+	return spp_node;
+}
+
+
+static int process_update_node(struct hs20_osu_client *ctx, xml_node_t *pps,
+			       xml_node_t *update)
+{
+	xml_node_t *node, *parent, *tnds, *unode;
+	char *str;
+	const char *name;
+	char *uri, *pos;
+	char *cdata, *cdata_end;
+	size_t fqdn_len;
+
+	wpa_printf(MSG_INFO, "Processing updateNode");
+	debug_dump_node(ctx, "updateNode", update);
+
+	uri = get_spp_attr_value(ctx->xml, update, "managementTreeURI");
+	if (uri == NULL) {
+		wpa_printf(MSG_INFO, "No managementTreeURI present");
+		return -1;
+	}
+	wpa_printf(MSG_INFO, "managementTreeUri: '%s'", uri);
+
+	name = os_strrchr(uri, '/');
+	if (name == NULL) {
+		wpa_printf(MSG_INFO, "Unexpected URI");
+		xml_node_get_attr_value_free(ctx->xml, uri);
+		return -1;
+	}
+	name++;
+	wpa_printf(MSG_INFO, "Update interior node: '%s'", name);
+
+	str = xml_node_get_text(ctx->xml, update);
+	if (str == NULL) {
+		wpa_printf(MSG_INFO, "Could not extract MO text");
+		xml_node_get_attr_value_free(ctx->xml, uri);
+		return -1;
+	}
+	wpa_printf(MSG_DEBUG, "[hs20] nodeContainer text: '%s'", str);
+	cdata = strstr(str, "<![CDATA[");
+	cdata_end = strstr(str, "]]>");
+	if (cdata && cdata_end && cdata_end > cdata &&
+	    cdata < strstr(str, "MgmtTree") &&
+	    cdata_end > strstr(str, "/MgmtTree")) {
+		char *tmp;
+		wpa_printf(MSG_DEBUG, "[hs20] Removing extra CDATA container");
+		tmp = strdup(cdata + 9);
+		if (tmp) {
+			cdata_end = strstr(tmp, "]]>");
+			if (cdata_end)
+				*cdata_end = '\0';
+			wpa_printf(MSG_DEBUG, "[hs20] nodeContainer text with CDATA container removed: '%s'",
+				   tmp);
+			tnds = xml_node_from_buf(ctx->xml, tmp);
+			free(tmp);
+		} else
+			tnds = NULL;
+	} else
+		tnds = xml_node_from_buf(ctx->xml, str);
+	xml_node_get_text_free(ctx->xml, str);
+	if (tnds == NULL) {
+		wpa_printf(MSG_INFO, "[hs20] Could not parse nodeContainer text");
+		xml_node_get_attr_value_free(ctx->xml, uri);
+		return -1;
+	}
+
+	unode = tnds_to_mo(ctx->xml, tnds);
+	xml_node_free(ctx->xml, tnds);
+	if (unode == NULL) {
+		wpa_printf(MSG_INFO, "[hs20] Could not parse nodeContainer TNDS text");
+		xml_node_get_attr_value_free(ctx->xml, uri);
+		return -1;
+	}
+
+	debug_dump_node(ctx, "Parsed TNDS", unode);
+
+	if (get_node_uri(ctx->xml, unode, name) == NULL) {
+		wpa_printf(MSG_INFO, "[hs20] %s node not found", name);
+		xml_node_free(ctx->xml, unode);
+		xml_node_get_attr_value_free(ctx->xml, uri);
+		return -1;
+	}
+
+	if (os_strncasecmp(uri, "./Wi-Fi/", 8) != 0) {
+		wpa_printf(MSG_INFO, "Do not allow update outside ./Wi-Fi");
+		xml_node_free(ctx->xml, unode);
+		xml_node_get_attr_value_free(ctx->xml, uri);
+		return -1;
+	}
+	pos = uri + 8;
+
+	if (ctx->fqdn == NULL) {
+		wpa_printf(MSG_INFO, "FQDN not known");
+		xml_node_free(ctx->xml, unode);
+		xml_node_get_attr_value_free(ctx->xml, uri);
+		return -1;
+	}
+	fqdn_len = os_strlen(ctx->fqdn);
+	if (os_strncasecmp(pos, ctx->fqdn, fqdn_len) != 0 ||
+	    pos[fqdn_len] != '/') {
+		wpa_printf(MSG_INFO, "Do not allow update outside ./Wi-Fi/%s",
+			   ctx->fqdn);
+		xml_node_free(ctx->xml, unode);
+		xml_node_get_attr_value_free(ctx->xml, uri);
+		return -1;
+	}
+	pos += fqdn_len + 1;
+
+	if (os_strncasecmp(pos, "PerProviderSubscription/", 24) != 0) {
+		wpa_printf(MSG_INFO, "Do not allow update outside ./Wi-Fi/%s/PerProviderSubscription",
+			   ctx->fqdn);
+		xml_node_free(ctx->xml, unode);
+		xml_node_get_attr_value_free(ctx->xml, uri);
+		return -1;
+	}
+	pos += 24;
+
+	wpa_printf(MSG_INFO, "Update command for PPS node %s", pos);
+
+	node = get_node(ctx->xml, pps, pos);
+	if (node) {
+		parent = xml_node_get_parent(ctx->xml, node);
+		xml_node_detach(ctx->xml, node);
+		wpa_printf(MSG_INFO, "Replace '%s' node", name);
+	} else {
+		char *pos2;
+		pos2 = os_strrchr(pos, '/');
+		if (pos2 == NULL) {
+			parent = pps;
+		} else {
+			*pos2 = '\0';
+			parent = get_node(ctx->xml, pps, pos);
+		}
+		if (parent == NULL) {
+			wpa_printf(MSG_INFO, "Could not find parent %s", pos);
+			xml_node_free(ctx->xml, unode);
+			xml_node_get_attr_value_free(ctx->xml, uri);
+			return -1;
+		}
+		wpa_printf(MSG_INFO, "Add '%s' node", name);
+	}
+	xml_node_add_child(ctx->xml, parent, unode);
+
+	xml_node_get_attr_value_free(ctx->xml, uri);
+
+	return 0;
+}
+
+
+static int update_pps(struct hs20_osu_client *ctx, xml_node_t *update,
+		      const char *pps_fname, xml_node_t *pps)
+{
+	wpa_printf(MSG_INFO, "Updating PPS based on updateNode element(s)");
+	xml_node_for_each_sibling(ctx->xml, update) {
+		xml_node_for_each_check(ctx->xml, update);
+		if (process_update_node(ctx, pps, update) < 0)
+			return -1;
+	}
+
+	return update_pps_file(ctx, pps_fname, pps);
+}
+
+
+static void hs20_sub_rem_complete(struct hs20_osu_client *ctx,
+				  const char *pps_fname)
+{
+	/*
+	 * Update wpa_supplicant credentials and reconnect using updated
+	 * information.
+	 */
+	wpa_printf(MSG_INFO, "Updating wpa_supplicant credentials");
+	cmd_set_pps(ctx, pps_fname);
+
+	if (ctx->no_reconnect)
+		return;
+
+	wpa_printf(MSG_INFO, "Requesting reconnection with updated configuration");
+	if (wpa_command(ctx->ifname, "INTERWORKING_SELECT auto") < 0)
+		wpa_printf(MSG_ERROR, "Failed to request wpa_supplicant to reconnect");
+}
+
+
+static xml_node_t * hs20_spp_upload_mo(struct hs20_osu_client *ctx,
+				       xml_node_t *cmd,
+				       const char *session_id,
+				       const char *pps_fname)
+{
+	xml_namespace_t *ns;
+	xml_node_t *node, *ret_node;
+	char *urn;
+
+	urn = get_spp_attr_value(ctx->xml, cmd, "moURN");
+	if (!urn) {
+		wpa_printf(MSG_INFO, "No URN included");
+		return NULL;
+	}
+	wpa_printf(MSG_INFO, "Upload MO request - URN=%s", urn);
+	if (strcasecmp(urn, URN_HS20_PPS) != 0) {
+		wpa_printf(MSG_INFO, "Unsupported moURN");
+		xml_node_get_attr_value_free(ctx->xml, urn);
+		return NULL;
+	}
+	xml_node_get_attr_value_free(ctx->xml, urn);
+
+	if (!pps_fname) {
+		wpa_printf(MSG_INFO, "PPS file name no known");
+		return NULL;
+	}
+
+	node = build_spp_post_dev_data(ctx, &ns, session_id,
+				       "MO upload");
+	if (node == NULL)
+		return NULL;
+	add_mo_container(ctx->xml, ns, node, URN_HS20_PPS, pps_fname);
+
+	ret_node = soap_send_receive(ctx->http, node);
+	if (ret_node == NULL)
+		return NULL;
+
+	debug_dump_node(ctx, "Received response to MO upload", ret_node);
+
+	if (hs20_spp_validate(ctx, ret_node, "sppPostDevDataResponse") < 0) {
+		wpa_printf(MSG_INFO, "SPP validation failed");
+		xml_node_free(ctx->xml, ret_node);
+		return NULL;
+	}
+
+	return ret_node;
+}
+
+
+static int hs20_add_mo(struct hs20_osu_client *ctx, xml_node_t *add_mo,
+		       char *fname, size_t fname_len)
+{
+	char *uri, *urn;
+	int ret;
+
+	debug_dump_node(ctx, "Received addMO", add_mo);
+
+	urn = get_spp_attr_value(ctx->xml, add_mo, "moURN");
+	if (urn == NULL) {
+		wpa_printf(MSG_INFO, "[hs20] No moURN in addMO");
+		return -1;
+	}
+	wpa_printf(MSG_INFO, "addMO - moURN: '%s'", urn);
+	if (strcasecmp(urn, URN_HS20_PPS) != 0) {
+		wpa_printf(MSG_INFO, "[hs20] Unsupported MO in addMO");
+		xml_node_get_attr_value_free(ctx->xml, urn);
+		return -1;
+	}
+	xml_node_get_attr_value_free(ctx->xml, urn);
+
+	uri = get_spp_attr_value(ctx->xml, add_mo, "managementTreeURI");
+	if (uri == NULL) {
+		wpa_printf(MSG_INFO, "[hs20] No managementTreeURI in addMO");
+		return -1;
+	}
+	wpa_printf(MSG_INFO, "addMO - managementTreeURI: '%s'", uri);
+
+	ret = hs20_add_pps_mo(ctx, uri, add_mo, fname, fname_len);
+	xml_node_get_attr_value_free(ctx->xml, uri);
+	return ret;
+}
+
+
+static int process_spp_user_input_response(struct hs20_osu_client *ctx,
+					   const char *session_id,
+					   xml_node_t *add_mo)
+{
+	int ret;
+	char fname[300];
+
+	debug_dump_node(ctx, "addMO", add_mo);
+
+	wpa_printf(MSG_INFO, "Subscription registration completed");
+
+	if (hs20_add_mo(ctx, add_mo, fname, sizeof(fname)) < 0) {
+		wpa_printf(MSG_INFO, "Could not add MO");
+		ret = hs20_spp_update_response(
+			ctx, session_id,
+			"Error occurred",
+			"MO addition or update failed");
+		return 0;
+	}
+
+	ret = hs20_spp_update_response(ctx, session_id, "OK", NULL);
+	if (ret == 0)
+		hs20_sub_rem_complete(ctx, fname);
+
+	return 0;
+}
+
+
+static xml_node_t * hs20_spp_user_input_completed(struct hs20_osu_client *ctx,
+						    const char *session_id)
+{
+	xml_node_t *node, *ret_node;
+
+	node = build_spp_post_dev_data(ctx, NULL, session_id,
+				       "User input completed");
+	if (node == NULL)
+		return NULL;
+
+	ret_node = soap_send_receive(ctx->http, node);
+	if (!ret_node) {
+		if (soap_reinit_client(ctx->http) < 0)
+			return NULL;
+		wpa_printf(MSG_INFO, "Try to finish with re-opened connection");
+		node = build_spp_post_dev_data(ctx, NULL, session_id,
+					       "User input completed");
+		if (node == NULL)
+			return NULL;
+		ret_node = soap_send_receive(ctx->http, node);
+		if (ret_node == NULL)
+			return NULL;
+		wpa_printf(MSG_INFO, "Continue with new connection");
+	}
+
+	if (hs20_spp_validate(ctx, ret_node, "sppPostDevDataResponse") < 0) {
+		wpa_printf(MSG_INFO, "SPP validation failed");
+		xml_node_free(ctx->xml, ret_node);
+		return NULL;
+	}
+
+	return ret_node;
+}
+
+
+static xml_node_t * hs20_spp_get_certificate(struct hs20_osu_client *ctx,
+					     xml_node_t *cmd,
+					     const char *session_id,
+					     const char *pps_fname)
+{
+	xml_namespace_t *ns;
+	xml_node_t *node, *ret_node;
+	int res;
+
+	wpa_printf(MSG_INFO, "Client certificate enrollment");
+
+	res = osu_get_certificate(ctx, cmd);
+	if (res < 0)
+		wpa_printf(MSG_INFO, "EST simpleEnroll failed");
+
+	node = build_spp_post_dev_data(ctx, &ns, session_id,
+				       res == 0 ?
+				       "Certificate enrollment completed" :
+				       "Certificate enrollment failed");
+	if (node == NULL)
+		return NULL;
+
+	ret_node = soap_send_receive(ctx->http, node);
+	if (ret_node == NULL)
+		return NULL;
+
+	debug_dump_node(ctx, "Received response to certificate enrollment "
+			"completed", ret_node);
+
+	if (hs20_spp_validate(ctx, ret_node, "sppPostDevDataResponse") < 0) {
+		wpa_printf(MSG_INFO, "SPP validation failed");
+		xml_node_free(ctx->xml, ret_node);
+		return NULL;
+	}
+
+	return ret_node;
+}
+
+
+static int hs20_spp_exec(struct hs20_osu_client *ctx, xml_node_t *exec,
+			 const char *session_id, const char *pps_fname,
+			 xml_node_t *pps, xml_node_t **ret_node)
+{
+	xml_node_t *cmd;
+	const char *name;
+	char *uri;
+	char *id = strdup(session_id);
+
+	if (id == NULL)
+		return -1;
+
+	*ret_node = NULL;
+
+	debug_dump_node(ctx, "exec", exec);
+
+	xml_node_for_each_child(ctx->xml, cmd, exec) {
+		xml_node_for_each_check(ctx->xml, cmd);
+		break;
+	}
+	if (!cmd) {
+		wpa_printf(MSG_INFO, "exec command element not found (cmd=%p)",
+			   cmd);
+		free(id);
+		return -1;
+	}
+
+	name = xml_node_get_localname(ctx->xml, cmd);
+
+	if (strcasecmp(name, "launchBrowserToURI") == 0) {
+		int res;
+		uri = xml_node_get_text(ctx->xml, cmd);
+		if (!uri) {
+			wpa_printf(MSG_INFO, "No URI found");
+			free(id);
+			return -1;
+		}
+		wpa_printf(MSG_INFO, "Launch browser to URI '%s'", uri);
+		write_summary(ctx, "Launch browser to URI '%s'", uri);
+		res = hs20_web_browser(uri);
+		xml_node_get_text_free(ctx->xml, uri);
+		if (res > 0) {
+			wpa_printf(MSG_INFO, "User response in browser completed successfully - sessionid='%s'",
+				   id);
+			write_summary(ctx, "User response in browser completed successfully");
+			*ret_node = hs20_spp_user_input_completed(ctx, id);
+			free(id);
+			return *ret_node ? 0 : -1;
+		} else {
+			wpa_printf(MSG_INFO, "Failed to receive user response");
+			write_summary(ctx, "Failed to receive user response");
+			hs20_spp_update_response(
+				ctx, id, "Error occurred", "Other");
+			free(id);
+			return -1;
+		}
+		return 0;
+	}
+
+	if (strcasecmp(name, "uploadMO") == 0) {
+		if (pps_fname == NULL)
+			return -1;
+		*ret_node = hs20_spp_upload_mo(ctx, cmd, id,
+					       pps_fname);
+		free(id);
+		return *ret_node ? 0 : -1;
+	}
+
+	if (strcasecmp(name, "getCertificate") == 0) {
+		*ret_node = hs20_spp_get_certificate(ctx, cmd, id,
+						     pps_fname);
+		free(id);
+		return *ret_node ? 0 : -1;
+	}
+
+	wpa_printf(MSG_INFO, "Unsupported exec command: '%s'", name);
+	free(id);
+	return -1;
+}
+
+
+enum spp_post_dev_data_use {
+	SPP_SUBSCRIPTION_REMEDIATION,
+	SPP_POLICY_UPDATE,
+	SPP_SUBSCRIPTION_REGISTRATION,
+};
+
+static void process_spp_post_dev_data_response(
+	struct hs20_osu_client *ctx,
+	enum spp_post_dev_data_use use, xml_node_t *node,
+	const char *pps_fname, xml_node_t *pps)
+{
+	xml_node_t *child;
+	char *status = NULL;
+	xml_node_t *update = NULL, *exec = NULL, *add_mo = NULL, *no_mo = NULL;
+	char *session_id = NULL;
+
+	debug_dump_node(ctx, "sppPostDevDataResponse node", node);
+
+	status = get_spp_attr_value(ctx->xml, node, "sppStatus");
+	if (status == NULL) {
+		wpa_printf(MSG_INFO, "No sppStatus attribute");
+		goto out;
+	}
+	write_summary(ctx, "Received sppPostDevDataResponse sppStatus='%s'",
+		      status);
+
+	session_id = get_spp_attr_value(ctx->xml, node, "sessionID");
+	if (session_id == NULL) {
+		wpa_printf(MSG_INFO, "No sessionID attribute");
+		goto out;
+	}
+
+	wpa_printf(MSG_INFO, "[hs20] sppPostDevDataResponse - sppStatus: '%s'  sessionID: '%s'",
+		   status, session_id);
+
+	xml_node_for_each_child(ctx->xml, child, node) {
+		const char *name;
+		xml_node_for_each_check(ctx->xml, child);
+		debug_dump_node(ctx, "child", child);
+		name = xml_node_get_localname(ctx->xml, child);
+		wpa_printf(MSG_INFO, "localname: '%s'", name);
+		if (!update && strcasecmp(name, "updateNode") == 0)
+			update = child;
+		if (!exec && strcasecmp(name, "exec") == 0)
+			exec = child;
+		if (!add_mo && strcasecmp(name, "addMO") == 0)
+			add_mo = child;
+		if (!no_mo && strcasecmp(name, "noMOUpdate") == 0)
+			no_mo = child;
+	}
+
+	if (use == SPP_SUBSCRIPTION_REMEDIATION &&
+	    strcasecmp(status,
+		       "Remediation complete, request sppUpdateResponse") == 0)
+	{
+		int res, ret;
+		if (!update && !no_mo) {
+			wpa_printf(MSG_INFO, "No updateNode or noMOUpdate element");
+			goto out;
+		}
+		wpa_printf(MSG_INFO, "Subscription remediation completed");
+		res = update_pps(ctx, update, pps_fname, pps);
+		if (res < 0)
+			wpa_printf(MSG_INFO, "Failed to update PPS MO");
+		ret = hs20_spp_update_response(
+			ctx, session_id,
+			res < 0 ? "Error occurred" : "OK",
+			res < 0 ? "MO addition or update failed" : NULL);
+		if (res == 0 && ret == 0)
+			hs20_sub_rem_complete(ctx, pps_fname);
+		goto out;
+	}
+
+	if (use == SPP_SUBSCRIPTION_REMEDIATION &&
+	    strcasecmp(status, "Exchange complete, release TLS connection") ==
+	    0) {
+		if (!no_mo) {
+			wpa_printf(MSG_INFO, "No noMOUpdate element");
+			goto out;
+		}
+		wpa_printf(MSG_INFO, "Subscription remediation completed (no MO update)");
+		goto out;
+	}
+
+	if (use == SPP_POLICY_UPDATE &&
+	    strcasecmp(status, "Update complete, request sppUpdateResponse") ==
+	    0) {
+		int res, ret;
+		wpa_printf(MSG_INFO, "Policy update received - update PPS");
+		res = update_pps(ctx, update, pps_fname, pps);
+		ret = hs20_spp_update_response(
+			ctx, session_id,
+			res < 0 ? "Error occurred" : "OK",
+			res < 0 ? "MO addition or update failed" : NULL);
+		if (res == 0 && ret == 0)
+			hs20_policy_update_complete(ctx, pps_fname);
+		goto out;
+	}
+
+	if (use == SPP_SUBSCRIPTION_REGISTRATION &&
+	    strcasecmp(status, "Provisioning complete, request "
+		       "sppUpdateResponse")  == 0) {
+		if (!add_mo) {
+			wpa_printf(MSG_INFO, "No addMO element - not sure what to do next");
+			goto out;
+		}
+		process_spp_user_input_response(ctx, session_id, add_mo);
+		node = NULL;
+		goto out;
+	}
+
+	if (strcasecmp(status, "No update available at this time") == 0) {
+		wpa_printf(MSG_INFO, "No update available at this time");
+		goto out;
+	}
+
+	if (strcasecmp(status, "OK") == 0) {
+		int res;
+		xml_node_t *ret;
+
+		if (!exec) {
+			wpa_printf(MSG_INFO, "No exec element - not sure what to do next");
+			goto out;
+		}
+		res = hs20_spp_exec(ctx, exec, session_id,
+				    pps_fname, pps, &ret);
+		/* xml_node_free(ctx->xml, node); */
+		node = NULL;
+		if (res == 0 && ret)
+			process_spp_post_dev_data_response(ctx, use,
+							   ret, pps_fname, pps);
+		goto out;
+	}
+
+	if (strcasecmp(status, "Error occurred") == 0) {
+		xml_node_t *err;
+		char *code = NULL;
+		err = get_node(ctx->xml, node, "sppError");
+		if (err)
+			code = xml_node_get_attr_value(ctx->xml, err,
+						       "errorCode");
+		wpa_printf(MSG_INFO, "Error occurred - errorCode=%s",
+			   code ? code : "N/A");
+		xml_node_get_attr_value_free(ctx->xml, code);
+		goto out;
+	}
+
+	wpa_printf(MSG_INFO,
+		   "[hs20] Unsupported sppPostDevDataResponse sppStatus '%s'",
+		   status);
+out:
+	xml_node_get_attr_value_free(ctx->xml, status);
+	xml_node_get_attr_value_free(ctx->xml, session_id);
+	xml_node_free(ctx->xml, node);
+}
+
+
+static int spp_post_dev_data(struct hs20_osu_client *ctx,
+			     enum spp_post_dev_data_use use,
+			     const char *reason,
+			     const char *pps_fname, xml_node_t *pps)
+{
+	xml_node_t *payload;
+	xml_node_t *ret_node;
+
+	payload = build_spp_post_dev_data(ctx, NULL, NULL, reason);
+	if (payload == NULL)
+		return -1;
+
+	ret_node = soap_send_receive(ctx->http, payload);
+	if (!ret_node) {
+		const char *err = http_get_err(ctx->http);
+		if (err) {
+			wpa_printf(MSG_INFO, "HTTP error: %s", err);
+			write_result(ctx, "HTTP error: %s", err);
+		} else {
+			write_summary(ctx, "Failed to send SOAP message");
+		}
+		return -1;
+	}
+
+	if (hs20_spp_validate(ctx, ret_node, "sppPostDevDataResponse") < 0) {
+		wpa_printf(MSG_INFO, "SPP validation failed");
+		xml_node_free(ctx->xml, ret_node);
+		return -1;
+	}
+
+	process_spp_post_dev_data_response(ctx, use, ret_node,
+					   pps_fname, pps);
+	return 0;
+}
+
+
+void spp_sub_rem(struct hs20_osu_client *ctx, const char *address,
+		 const char *pps_fname, const char *ca_fname,
+		 const char *client_cert, const char *client_key,
+		 const char *cred_username, const char *cred_password,
+		 xml_node_t *pps)
+{
+	wpa_printf(MSG_INFO, "SPP subscription remediation");
+	write_summary(ctx, "SPP subscription remediation");
+
+	os_free(ctx->server_url);
+	ctx->server_url = os_strdup(address);
+
+	if (soap_init_client(ctx->http, address, ca_fname,
+			     cred_username, cred_password, client_cert,
+			     client_key) == 0) {
+		spp_post_dev_data(ctx, SPP_SUBSCRIPTION_REMEDIATION,
+				  "Subscription remediation", pps_fname, pps);
+	}
+}
+
+
+static void hs20_policy_update_complete(struct hs20_osu_client *ctx,
+					const char *pps_fname)
+{
+	wpa_printf(MSG_INFO, "Policy update completed");
+
+	/*
+	 * Update wpa_supplicant credentials and reconnect using updated
+	 * information.
+	 */
+	wpa_printf(MSG_INFO, "Updating wpa_supplicant credentials");
+	cmd_set_pps(ctx, pps_fname);
+
+	wpa_printf(MSG_INFO, "Requesting reconnection with updated configuration");
+	if (wpa_command(ctx->ifname, "INTERWORKING_SELECT auto") < 0)
+		wpa_printf(MSG_ERROR, "Failed to request wpa_supplicant to reconnect");
+}
+
+
+static int process_spp_exchange_complete(struct hs20_osu_client *ctx,
+					 xml_node_t *node)
+{
+	char *status, *session_id;
+
+	debug_dump_node(ctx, "sppExchangeComplete", node);
+
+	status = get_spp_attr_value(ctx->xml, node, "sppStatus");
+	if (status == NULL) {
+		wpa_printf(MSG_INFO, "No sppStatus attribute");
+		return -1;
+	}
+	write_summary(ctx, "Received sppExchangeComplete sppStatus='%s'",
+		      status);
+
+	session_id = get_spp_attr_value(ctx->xml, node, "sessionID");
+	if (session_id == NULL) {
+		wpa_printf(MSG_INFO, "No sessionID attribute");
+		xml_node_get_attr_value_free(ctx->xml, status);
+		return -1;
+	}
+
+	wpa_printf(MSG_INFO, "[hs20] sppStatus: '%s'  sessionID: '%s'",
+		   status, session_id);
+	xml_node_get_attr_value_free(ctx->xml, session_id);
+
+	if (strcasecmp(status, "Exchange complete, release TLS connection") ==
+	    0) {
+		xml_node_get_attr_value_free(ctx->xml, status);
+		return 0;
+	}
+
+	wpa_printf(MSG_INFO, "Unexpected sppStatus '%s'", status);
+	write_summary(ctx, "Unexpected sppStatus '%s'", status);
+	xml_node_get_attr_value_free(ctx->xml, status);
+	return -1;
+}
+
+
+static xml_node_t * build_spp_update_response(struct hs20_osu_client *ctx,
+					      const char *session_id,
+					      const char *spp_status,
+					      const char *error_code)
+{
+	xml_namespace_t *ns;
+	xml_node_t *spp_node, *node;
+
+	spp_node = xml_node_create_root(ctx->xml, SPP_NS_URI, "spp", &ns,
+					"sppUpdateResponse");
+	if (spp_node == NULL)
+		return NULL;
+
+	xml_node_add_attr(ctx->xml, spp_node, ns, "sppVersion", "1.0");
+	xml_node_add_attr(ctx->xml, spp_node, ns, "sessionID", session_id);
+	xml_node_add_attr(ctx->xml, spp_node, ns, "sppStatus", spp_status);
+
+	if (error_code) {
+		node = xml_node_create(ctx->xml, spp_node, ns, "sppError");
+		if (node)
+			xml_node_add_attr(ctx->xml, node, NULL, "errorCode",
+					  error_code);
+	}
+
+	return spp_node;
+}
+
+
+static int hs20_spp_update_response(struct hs20_osu_client *ctx,
+				    const char *session_id,
+				    const char *spp_status,
+				    const char *error_code)
+{
+	xml_node_t *node, *ret_node;
+	int ret;
+
+	write_summary(ctx, "Building sppUpdateResponse sppStatus='%s' error_code='%s'",
+		      spp_status, error_code);
+	node = build_spp_update_response(ctx, session_id, spp_status,
+					 error_code);
+	if (node == NULL)
+		return -1;
+	ret_node = soap_send_receive(ctx->http, node);
+	if (!ret_node) {
+		if (soap_reinit_client(ctx->http) < 0)
+			return -1;
+		wpa_printf(MSG_INFO, "Try to finish with re-opened connection");
+		node = build_spp_update_response(ctx, session_id, spp_status,
+						 error_code);
+		if (node == NULL)
+			return -1;
+		ret_node = soap_send_receive(ctx->http, node);
+		if (ret_node == NULL)
+			return -1;
+		wpa_printf(MSG_INFO, "Continue with new connection");
+	}
+
+	if (hs20_spp_validate(ctx, ret_node, "sppExchangeComplete") < 0) {
+		wpa_printf(MSG_INFO, "SPP validation failed");
+		xml_node_free(ctx->xml, ret_node);
+		return -1;
+	}
+
+	ret = process_spp_exchange_complete(ctx, ret_node);
+	xml_node_free(ctx->xml, ret_node);
+	return ret;
+}
+
+
+void spp_pol_upd(struct hs20_osu_client *ctx, const char *address,
+		 const char *pps_fname, const char *ca_fname,
+		 const char *client_cert, const char *client_key,
+		 const char *cred_username, const char *cred_password,
+		 xml_node_t *pps)
+{
+	wpa_printf(MSG_INFO, "SPP policy update");
+	write_summary(ctx, "SPP policy update");
+
+	os_free(ctx->server_url);
+	ctx->server_url = os_strdup(address);
+
+	if (soap_init_client(ctx->http, address, ca_fname, cred_username,
+			     cred_password, client_cert, client_key) == 0) {
+		spp_post_dev_data(ctx, SPP_POLICY_UPDATE, "Policy update",
+				  pps_fname, pps);
+	}
+}
+
+
+int cmd_prov(struct hs20_osu_client *ctx, const char *url,
+	     const char *ca_fname)
+{
+	unlink("Cert/est_cert.der");
+	unlink("Cert/est_cert.pem");
+
+	ctx->ca_fname = ca_fname;
+
+	if (url == NULL) {
+		wpa_printf(MSG_INFO, "Invalid prov command (missing URL)");
+		return -1;
+	}
+
+	wpa_printf(MSG_INFO, "Credential provisioning requested");
+
+	os_free(ctx->server_url);
+	ctx->server_url = os_strdup(url);
+
+	if (soap_init_client(ctx->http, url, ca_fname, NULL, NULL, NULL, NULL) <
+	    0)
+		return -1;
+	spp_post_dev_data(ctx, SPP_SUBSCRIPTION_REGISTRATION,
+			  "Subscription registration", NULL, NULL);
+
+	return ctx->pps_cred_set ? 0 : -1;
+}
+
+
+int cmd_sim_prov(struct hs20_osu_client *ctx, const char *url,
+	     const char *ca_fname)
+{
+	ctx->ca_fname = ca_fname;
+
+	if (url == NULL) {
+		wpa_printf(MSG_INFO, "Invalid prov command (missing URL)");
+		return -1;
+	}
+
+	wpa_printf(MSG_INFO, "SIM provisioning requested");
+
+	os_free(ctx->server_url);
+	ctx->server_url = os_strdup(url);
+
+	wpa_printf(MSG_INFO, "Wait for IP address before starting SIM provisioning");
+
+	if (wait_ip_addr(ctx->ifname, 15) < 0) {
+		wpa_printf(MSG_INFO, "Could not get IP address for WLAN - try connection anyway");
+	}
+
+	if (soap_init_client(ctx->http, url, ca_fname, NULL, NULL, NULL, NULL) <
+	    0)
+		return -1;
+	spp_post_dev_data(ctx, SPP_SUBSCRIPTION_REGISTRATION,
+			  "Subscription provisioning", NULL, NULL);
+
+	return ctx->pps_cred_set ? 0 : -1;
+}
diff --git a/src/common/wpa_helpers.c b/src/common/wpa_helpers.c
new file mode 100644
index 0000000..28913b9
--- /dev/null
+++ b/src/common/wpa_helpers.c
@@ -0,0 +1,292 @@
+/*
+ * wpa_supplicant ctrl_iface helpers
+ * Copyright (c) 2010-2011, Atheros Communications, Inc.
+ * Copyright (c) 2011-2012, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+#include <time.h>
+
+#include "common.h"
+#include "wpa_ctrl.h"
+#include "wpa_helpers.h"
+
+
+char *wpas_ctrl_path = "/var/run/wpa_supplicant/";
+static int default_timeout = 60;
+
+
+static struct wpa_ctrl * wpa_open_ctrl(const char *ifname)
+{
+	char buf[128];
+	struct wpa_ctrl *ctrl;
+
+	os_snprintf(buf, sizeof(buf), "%s%s", wpas_ctrl_path, ifname);
+	ctrl = wpa_ctrl_open(buf);
+	if (ctrl == NULL)
+		printf("wpa_command: wpa_ctrl_open(%s) failed\n", buf);
+	return ctrl;
+}
+
+
+int wpa_command(const char *ifname, const char *cmd)
+{
+	struct wpa_ctrl *ctrl;
+	char buf[128];
+	size_t len;
+
+	printf("wpa_command(ifname='%s', cmd='%s')\n", ifname, cmd);
+	ctrl = wpa_open_ctrl(ifname);
+	if (ctrl == NULL)
+		return -1;
+	len = sizeof(buf);
+	if (wpa_ctrl_request(ctrl, cmd, strlen(cmd), buf, &len, NULL) < 0) {
+		printf("wpa_command: wpa_ctrl_request failed\n");
+		wpa_ctrl_close(ctrl);
+		return -1;
+	}
+	wpa_ctrl_close(ctrl);
+	buf[len] = '\0';
+	if (strncmp(buf, "FAIL", 4) == 0) {
+		printf("wpa_command: Command failed (FAIL received)\n");
+		return -1;
+	}
+	return 0;
+}
+
+
+int wpa_command_resp(const char *ifname, const char *cmd,
+		     char *resp, size_t resp_size)
+{
+	struct wpa_ctrl *ctrl;
+	size_t len;
+
+	printf("wpa_command(ifname='%s', cmd='%s')\n", ifname, cmd);
+	ctrl = wpa_open_ctrl(ifname);
+	if (ctrl == NULL)
+		return -1;
+	len = resp_size;
+	if (wpa_ctrl_request(ctrl, cmd, strlen(cmd), resp, &len, NULL) < 0) {
+		printf("wpa_command: wpa_ctrl_request failed\n");
+		wpa_ctrl_close(ctrl);
+		return -1;
+	}
+	wpa_ctrl_close(ctrl);
+	resp[len] = '\0';
+	return 0;
+}
+
+
+struct wpa_ctrl * open_wpa_mon(const char *ifname)
+{
+	struct wpa_ctrl *ctrl;
+
+	ctrl = wpa_open_ctrl(ifname);
+	if (ctrl == NULL)
+		return NULL;
+	if (wpa_ctrl_attach(ctrl) < 0) {
+		wpa_ctrl_close(ctrl);
+		return NULL;
+	}
+
+	return ctrl;
+}
+
+
+int get_wpa_cli_event2(struct wpa_ctrl *mon,
+		       const char *event, const char *event2,
+		       char *buf, size_t buf_size)
+{
+	int fd, ret;
+	fd_set rfd;
+	char *pos;
+	struct timeval tv;
+	time_t start, now;
+
+	printf("Waiting for wpa_cli event %s\n", event);
+	fd = wpa_ctrl_get_fd(mon);
+	if (fd < 0)
+		return -1;
+
+	time(&start);
+	while (1) {
+		size_t len;
+
+		FD_ZERO(&rfd);
+		FD_SET(fd, &rfd);
+		tv.tv_sec = default_timeout;
+		tv.tv_usec = 0;
+		ret = select(fd + 1, &rfd, NULL, NULL, &tv);
+		if (ret == 0) {
+			printf("Timeout on waiting for event %s\n", event);
+			return -1;
+		}
+		if (ret < 0) {
+			printf("select: %s\n", strerror(errno));
+			return -1;
+		}
+		len = buf_size;
+		if (wpa_ctrl_recv(mon, buf, &len) < 0) {
+			printf("Failure while waiting for event %s\n", event);
+			return -1;
+		}
+		if (len == buf_size)
+			len--;
+		buf[len] = '\0';
+
+		pos = strchr(buf, '>');
+		if (pos &&
+		    (strncmp(pos + 1, event, strlen(event)) == 0 ||
+		     (event2 &&
+		      strncmp(pos + 1, event2, strlen(event2)) == 0)))
+			return 0; /* Event found */
+
+		time(&now);
+		if ((int) (now - start) > default_timeout) {
+			printf("Timeout on waiting for event %s\n", event);
+			return -1;
+		}
+	}
+}
+
+
+int get_wpa_cli_event(struct wpa_ctrl *mon,
+		      const char *event, char *buf, size_t buf_size)
+{
+	return get_wpa_cli_event2(mon, event, NULL, buf, buf_size);
+}
+
+
+int get_wpa_status(const char *ifname, const char *field, char *obuf,
+		   size_t obuf_size)
+{
+	struct wpa_ctrl *ctrl;
+	char buf[4096];
+	char *pos, *end;
+	size_t len, flen;
+
+	ctrl = wpa_open_ctrl(ifname);
+	if (ctrl == NULL)
+		return -1;
+	len = sizeof(buf);
+	if (wpa_ctrl_request(ctrl, "STATUS", 6, buf, &len, NULL) < 0) {
+		wpa_ctrl_close(ctrl);
+		return -1;
+	}
+	wpa_ctrl_close(ctrl);
+	buf[len] = '\0';
+
+	flen = strlen(field);
+	pos = buf;
+	while (pos + flen < buf + len) {
+		if (pos > buf) {
+			if (*pos != '\n') {
+				pos++;
+				continue;
+			}
+			pos++;
+		}
+		if (strncmp(pos, field, flen) != 0 || pos[flen] != '=') {
+			pos++;
+			continue;
+		}
+		pos += flen + 1;
+		end = strchr(pos, '\n');
+		if (end == NULL)
+			return -1;
+		*end++ = '\0';
+		if (end - pos > (int) obuf_size)
+			return -1;
+		memcpy(obuf, pos, end - pos);
+		return 0;
+	}
+
+	return -1;
+}
+
+
+int wait_ip_addr(const char *ifname, int timeout)
+{
+	char ip[30];
+	int count = timeout;
+	struct wpa_ctrl *ctrl;
+
+	while (count > 0) {
+		printf("%s: ifname='%s' - %d seconds remaining\n",
+		       __func__, ifname, count);
+		count--;
+		if (get_wpa_status(ifname, "ip_address", ip, sizeof(ip)) == 0
+		    && strlen(ip) > 0) {
+			printf("IP address found: '%s'\n", ip);
+			return 0;
+		}
+		ctrl = wpa_open_ctrl(ifname);
+		if (ctrl == NULL)
+			return -1;
+		wpa_ctrl_close(ctrl);
+		sleep(1);
+	}
+	printf("%s: Could not get IP address for ifname='%s'", __func__,
+	       ifname);
+	return -1;
+}
+
+
+int add_network(const char *ifname)
+{
+	char res[30];
+
+	if (wpa_command_resp(ifname, "ADD_NETWORK", res, sizeof(res)) < 0)
+		return -1;
+	return atoi(res);
+}
+
+
+int set_network(const char *ifname, int id, const char *field,
+		const char *value)
+{
+	char buf[200];
+	snprintf(buf, sizeof(buf), "SET_NETWORK %d %s %s", id, field, value);
+	return wpa_command(ifname, buf);
+}
+
+
+int set_network_quoted(const char *ifname, int id, const char *field,
+		       const char *value)
+{
+	char buf[200];
+	snprintf(buf, sizeof(buf), "SET_NETWORK %d %s \"%s\"",
+		 id, field, value);
+	return wpa_command(ifname, buf);
+}
+
+
+int add_cred(const char *ifname)
+{
+	char res[30];
+
+	if (wpa_command_resp(ifname, "ADD_CRED", res, sizeof(res)) < 0)
+		return -1;
+	return atoi(res);
+}
+
+
+int set_cred(const char *ifname, int id, const char *field, const char *value)
+{
+	char buf[200];
+	snprintf(buf, sizeof(buf), "SET_CRED %d %s %s", id, field, value);
+	return wpa_command(ifname, buf);
+}
+
+
+int set_cred_quoted(const char *ifname, int id, const char *field,
+		    const char *value)
+{
+	char buf[200];
+	snprintf(buf, sizeof(buf), "SET_CRED %d %s \"%s\"",
+		 id, field, value);
+	return wpa_command(ifname, buf);
+}
diff --git a/src/common/wpa_helpers.h b/src/common/wpa_helpers.h
new file mode 100644
index 0000000..54c2872
--- /dev/null
+++ b/src/common/wpa_helpers.h
@@ -0,0 +1,37 @@
+/*
+ * wpa_supplicant ctrl_iface helpers
+ * Copyright (c) 2010-2011, Atheros Communications, Inc.
+ * Copyright (c) 2011-2012, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#ifndef WPA_HELPERS_H
+#define WPA_HELPERS_H
+
+int wpa_command(const char *ifname, const char *cmd);
+int wpa_command_resp(const char *ifname, const char *cmd,
+		     char *resp, size_t resp_size);
+int get_wpa_status(const char *ifname, const char *field, char *obuf,
+		   size_t obuf_size);
+
+struct wpa_ctrl * open_wpa_mon(const char *ifname);
+int wait_ip_addr(const char *ifname, int timeout);
+int get_wpa_cli_event(struct wpa_ctrl *mon,
+		      const char *event, char *buf, size_t buf_size);
+int get_wpa_cli_event2(struct wpa_ctrl *mon,
+		       const char *event, const char *event2,
+		       char *buf, size_t buf_size);
+
+int add_network(const char *ifname);
+int set_network(const char *ifname, int id, const char *field,
+		const char *value);
+int set_network_quoted(const char *ifname, int id, const char *field,
+		       const char *value);
+int add_cred(const char *ifname);
+int set_cred(const char *ifname, int id, const char *field, const char *value);
+int set_cred_quoted(const char *ifname, int id, const char *field,
+		    const char *value);
+
+#endif /* WPA_HELPERS_H */
diff --git a/src/utils/browser-android.c b/src/utils/browser-android.c
new file mode 100644
index 0000000..a066392
--- /dev/null
+++ b/src/utils/browser-android.c
@@ -0,0 +1,117 @@
+/*
+ * Hotspot 2.0 client - Web browser using Android browser
+ * Copyright (c) 2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+
+#include "common.h"
+#include "utils/eloop.h"
+#include "wps/http_server.h"
+#include "browser.h"
+
+
+struct browser_data {
+	int success;
+};
+
+
+static void browser_timeout(void *eloop_data, void *user_ctx)
+{
+	wpa_printf(MSG_INFO, "Timeout on waiting browser interaction to "
+		   "complete");
+	eloop_terminate();
+}
+
+
+static void http_req(void *ctx, struct http_request *req)
+{
+	struct browser_data *data = ctx;
+	struct wpabuf *resp;
+	const char *url;
+	int done = 0;
+
+	url = http_request_get_uri(req);
+	wpa_printf(MSG_INFO, "Browser response received: %s", url);
+
+	if (os_strcmp(url, "/") == 0) {
+		data->success = 1;
+		done = 1;
+	} else if (os_strncmp(url, "/osu/", 5) == 0) {
+		data->success = atoi(url + 5);
+		done = 1;
+	}
+
+	resp = wpabuf_alloc(1);
+	if (resp == NULL) {
+		http_request_deinit(req);
+		if (done)
+			eloop_terminate();
+		return;
+	}
+
+	if (done) {
+		eloop_cancel_timeout(browser_timeout, NULL, NULL);
+		eloop_register_timeout(0, 500000, browser_timeout, &data, NULL);
+	}
+
+	http_request_send_and_deinit(req, resp);
+}
+
+
+int hs20_web_browser(const char *url)
+{
+	char cmd[2000];
+	int ret;
+	struct http_server *http;
+	struct in_addr addr;
+	struct browser_data data;
+
+	wpa_printf(MSG_INFO, "Launching Android browser to %s", url);
+
+	os_memset(&data, 0, sizeof(data));
+
+	ret = os_snprintf(cmd, sizeof(cmd),
+			  "am start -a android.intent.action.VIEW -d '%s' "
+			  "-n com.android.browser/.BrowserActivity", url);
+	if (ret < 0 || (size_t) ret >= sizeof(cmd)) {
+		wpa_printf(MSG_ERROR, "Too long URL");
+		return -1;
+	}
+
+	if (eloop_init() < 0) {
+		wpa_printf(MSG_ERROR, "eloop_init failed");
+		return -1;
+	}
+	addr.s_addr = htonl((127 << 24) | 1);
+	http = http_server_init(&addr, 12345, http_req, &data);
+	if (http == NULL) {
+		wpa_printf(MSG_ERROR, "http_server_init failed");
+		eloop_destroy();
+		return -1;
+	}
+
+	if (system(cmd) != 0) {
+		wpa_printf(MSG_INFO, "Failed to launch Android browser");
+		eloop_cancel_timeout(browser_timeout, NULL, NULL);
+		http_server_deinit(http);
+		eloop_destroy();
+		return -1;
+	}
+
+	eloop_register_timeout(30, 0, browser_timeout, &data, NULL);
+	eloop_run();
+	eloop_cancel_timeout(browser_timeout, &data, NULL);
+	http_server_deinit(http);
+	eloop_destroy();
+
+	wpa_printf(MSG_INFO, "Closing Android browser");
+	if (system("input keyevent 3") != 0) {
+		wpa_printf(MSG_INFO, "Failed to inject keyevent");
+	}
+
+	return data.success;
+}
diff --git a/src/utils/browser-system.c b/src/utils/browser-system.c
new file mode 100644
index 0000000..2884d34
--- /dev/null
+++ b/src/utils/browser-system.c
@@ -0,0 +1,112 @@
+/*
+ * Hotspot 2.0 client - Web browser using system browser
+ * Copyright (c) 2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+
+#include "common.h"
+#include "utils/eloop.h"
+#include "wps/http_server.h"
+#include "browser.h"
+
+
+struct browser_data {
+	int success;
+};
+
+
+static void browser_timeout(void *eloop_data, void *user_ctx)
+{
+	wpa_printf(MSG_INFO, "Timeout on waiting browser interaction to "
+		   "complete");
+	eloop_terminate();
+}
+
+
+static void http_req(void *ctx, struct http_request *req)
+{
+	struct browser_data *data = ctx;
+	struct wpabuf *resp;
+	const char *url;
+	int done = 0;
+
+	url = http_request_get_uri(req);
+	wpa_printf(MSG_INFO, "Browser response received: %s", url);
+
+	if (os_strcmp(url, "/") == 0) {
+		data->success = 1;
+		done = 1;
+	} else if (os_strncmp(url, "/osu/", 5) == 0) {
+		data->success = atoi(url + 5);
+		done = 1;
+	}
+
+	resp = wpabuf_alloc(1);
+	if (resp == NULL) {
+		http_request_deinit(req);
+		if (done)
+			eloop_terminate();
+		return;
+	}
+
+	if (done) {
+		eloop_cancel_timeout(browser_timeout, NULL, NULL);
+		eloop_register_timeout(0, 500000, browser_timeout, &data, NULL);
+	}
+
+	http_request_send_and_deinit(req, resp);
+}
+
+
+int hs20_web_browser(const char *url)
+{
+	char cmd[2000];
+	int ret;
+	struct http_server *http;
+	struct in_addr addr;
+	struct browser_data data;
+
+	wpa_printf(MSG_INFO, "Launching Android browser to %s", url);
+
+	os_memset(&data, 0, sizeof(data));
+
+	ret = os_snprintf(cmd, sizeof(cmd), "x-www-browser '%s' &", url);
+	if (ret < 0 || (size_t) ret >= sizeof(cmd)) {
+		wpa_printf(MSG_ERROR, "Too long URL");
+		return -1;
+	}
+
+	if (eloop_init() < 0) {
+		wpa_printf(MSG_ERROR, "eloop_init failed");
+		return -1;
+	}
+	addr.s_addr = htonl((127 << 24) | 1);
+	http = http_server_init(&addr, 12345, http_req, &data);
+	if (http == NULL) {
+		wpa_printf(MSG_ERROR, "http_server_init failed");
+		eloop_destroy();
+		return -1;
+	}
+
+	if (system(cmd) != 0) {
+		wpa_printf(MSG_INFO, "Failed to launch browser");
+		eloop_cancel_timeout(browser_timeout, NULL, NULL);
+		http_server_deinit(http);
+		eloop_destroy();
+		return -1;
+	}
+
+	eloop_register_timeout(120, 0, browser_timeout, &data, NULL);
+	eloop_run();
+	eloop_cancel_timeout(browser_timeout, &data, NULL);
+	http_server_deinit(http);
+	eloop_destroy();
+
+	/* TODO: Close browser */
+
+	return data.success;
+}
diff --git a/src/utils/browser-wpadebug.c b/src/utils/browser-wpadebug.c
new file mode 100644
index 0000000..eeb8f65
--- /dev/null
+++ b/src/utils/browser-wpadebug.c
@@ -0,0 +1,123 @@
+/*
+ * Hotspot 2.0 client - Web browser using wpadebug on Android
+ * Copyright (c) 2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+
+#include "common.h"
+#include "utils/eloop.h"
+#include "wps/http_server.h"
+#include "browser.h"
+
+
+struct browser_data {
+	int success;
+};
+
+
+static void browser_timeout(void *eloop_data, void *user_ctx)
+{
+	wpa_printf(MSG_INFO, "Timeout on waiting browser interaction to "
+		   "complete");
+	eloop_terminate();
+}
+
+
+static void http_req(void *ctx, struct http_request *req)
+{
+	struct browser_data *data = ctx;
+	struct wpabuf *resp;
+	const char *url;
+	int done = 0;
+
+	url = http_request_get_uri(req);
+	wpa_printf(MSG_INFO, "Browser response received: %s", url);
+
+	if (os_strcmp(url, "/") == 0) {
+		data->success = 1;
+		done = 1;
+	} else if (os_strncmp(url, "/osu/", 5) == 0) {
+		data->success = atoi(url + 5);
+		done = 1;
+	}
+
+	resp = wpabuf_alloc(100);
+	if (resp == NULL) {
+		http_request_deinit(req);
+		if (done)
+			eloop_terminate();
+		return;
+	}
+	wpabuf_put_str(resp, "User input completed");
+
+	if (done) {
+		eloop_cancel_timeout(browser_timeout, NULL, NULL);
+		eloop_register_timeout(0, 500000, browser_timeout, &data, NULL);
+	}
+
+	http_request_send_and_deinit(req, resp);
+}
+
+
+int hs20_web_browser(const char *url)
+{
+	char cmd[2000];
+	int ret;
+	struct http_server *http;
+	struct in_addr addr;
+	struct browser_data data;
+
+	wpa_printf(MSG_INFO, "Launching wpadebug browser to %s", url);
+
+	os_memset(&data, 0, sizeof(data));
+
+	ret = os_snprintf(cmd, sizeof(cmd),
+			  "am start -a android.action.MAIN "
+			  "-c android.intent.category.LAUNCHER "
+			  "-n w1.fi.wpadebug/.WpaWebViewActivity "
+			  "-e w1.fi.wpadebug.URL '%s'", url);
+	if (ret < 0 || (size_t) ret >= sizeof(cmd)) {
+		wpa_printf(MSG_ERROR, "Too long URL");
+		return -1;
+	}
+
+	if (eloop_init() < 0) {
+		wpa_printf(MSG_ERROR, "eloop_init failed");
+		return -1;
+	}
+	addr.s_addr = htonl((127 << 24) | 1);
+	http = http_server_init(&addr, 12345, http_req, &data);
+	if (http == NULL) {
+		wpa_printf(MSG_ERROR, "http_server_init failed");
+		eloop_destroy();
+		return -1;
+	}
+
+	if (system(cmd) != 0) {
+		wpa_printf(MSG_INFO, "Failed to launch wpadebug browser");
+		eloop_cancel_timeout(browser_timeout, NULL, NULL);
+		http_server_deinit(http);
+		eloop_destroy();
+		return -1;
+	}
+
+	eloop_register_timeout(300, 0, browser_timeout, &data, NULL);
+	eloop_run();
+	eloop_cancel_timeout(browser_timeout, &data, NULL);
+	http_server_deinit(http);
+	eloop_destroy();
+
+	wpa_printf(MSG_INFO, "Closing Android browser");
+	if (system("am start -a android.action.MAIN "
+		   "-c android.intent.category.LAUNCHER "
+		   "-n w1.fi.wpadebug/.WpaWebViewActivity "
+		   "-e w1.fi.wpadebug.URL FINISH") != 0) {
+		wpa_printf(MSG_INFO, "Failed to close wpadebug browser");
+	}
+
+	return data.success;
+}
diff --git a/src/utils/browser.c b/src/utils/browser.c
new file mode 100644
index 0000000..9cf6152
--- /dev/null
+++ b/src/utils/browser.c
@@ -0,0 +1,219 @@
+/*
+ * Hotspot 2.0 client - Web browser using WebKit
+ * Copyright (c) 2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+#include <webkit/webkit.h>
+
+#include "common.h"
+#include "browser.h"
+
+
+struct browser_context {
+	GtkWidget *win;
+	int success;
+	int progress;
+	char *hover_link;
+	char *title;
+};
+
+static void win_cb_destroy(GtkWidget *win, struct browser_context *ctx)
+{
+	wpa_printf(MSG_DEBUG, "BROWSER:%s", __func__);
+	gtk_main_quit();
+}
+
+
+static void browser_update_title(struct browser_context *ctx)
+{
+	char buf[100];
+
+	if (ctx->hover_link) {
+		gtk_window_set_title(GTK_WINDOW(ctx->win), ctx->hover_link);
+		return;
+	}
+
+	if (ctx->progress == 100) {
+		gtk_window_set_title(GTK_WINDOW(ctx->win),
+				     ctx->title ? ctx->title :
+				     "Hotspot 2.0 client");
+		return;
+	}
+
+	snprintf(buf, sizeof(buf), "[%d%%] %s", ctx->progress,
+		 ctx->title ? ctx->title : "Hotspot 2.0 client");
+	gtk_window_set_title(GTK_WINDOW(ctx->win), buf);
+}
+
+
+static void view_cb_notify_progress(WebKitWebView *view, GParamSpec *pspec,
+				    struct browser_context *ctx)
+{
+	ctx->progress = 100 * webkit_web_view_get_progress(view);
+	wpa_printf(MSG_DEBUG, "BROWSER:%s progress=%d", __func__,
+		   ctx->progress);
+	browser_update_title(ctx);
+}
+
+
+static void view_cb_notify_load_status(WebKitWebView *view, GParamSpec *pspec,
+				       struct browser_context *ctx)
+{
+	int status = webkit_web_view_get_load_status(view);
+	wpa_printf(MSG_DEBUG, "BROWSER:%s load-status=%d uri=%s",
+		   __func__, status, webkit_web_view_get_uri(view));
+}
+
+
+static void view_cb_resource_request_starting(WebKitWebView *view,
+					      WebKitWebFrame *frame,
+					      WebKitWebResource *res,
+					      WebKitNetworkRequest *req,
+					      WebKitNetworkResponse *resp,
+					      struct browser_context *ctx)
+{
+	const gchar *uri = webkit_network_request_get_uri(req);
+	wpa_printf(MSG_DEBUG, "BROWSER:%s uri=%s", __func__, uri);
+	if (g_str_has_suffix(uri, "/favicon.ico"))
+		webkit_network_request_set_uri(req, "about:blank");
+	if (g_str_has_prefix(uri, "osu://")) {
+		ctx->success = atoi(uri + 6);
+		gtk_main_quit();
+	}
+	if (g_str_has_prefix(uri, "http://localhost:12345")) {
+		/*
+		 * This is used as a special trigger to indicate that the
+		 * user exchange has been completed.
+		 */
+		ctx->success = 1;
+		gtk_main_quit();
+	}
+}
+
+
+static gboolean view_cb_mime_type_policy_decision(
+	WebKitWebView *view, WebKitWebFrame *frame, WebKitNetworkRequest *req,
+	gchar *mime, WebKitWebPolicyDecision *policy,
+	struct browser_context *ctx)
+{
+	wpa_printf(MSG_DEBUG, "BROWSER:%s mime=%s", __func__, mime);
+
+	if (!webkit_web_view_can_show_mime_type(view, mime)) {
+		webkit_web_policy_decision_download(policy);
+		return TRUE;
+	}
+
+	return FALSE;
+}
+
+
+static gboolean view_cb_download_requested(WebKitWebView *view,
+					   WebKitDownload *dl,
+					   struct browser_context *ctx)
+{
+	const gchar *uri;
+	uri = webkit_download_get_uri(dl);
+	wpa_printf(MSG_DEBUG, "BROWSER:%s uri=%s", __func__, uri);
+	return FALSE;
+}
+
+
+static void view_cb_hovering_over_link(WebKitWebView *view, gchar *title,
+				       gchar *uri, struct browser_context *ctx)
+{
+	wpa_printf(MSG_DEBUG, "BROWSER:%s title=%s uri=%s", __func__, title,
+		   uri);
+	os_free(ctx->hover_link);
+	if (uri)
+		ctx->hover_link = os_strdup(uri);
+	else
+		ctx->hover_link = NULL;
+
+	browser_update_title(ctx);
+}
+
+
+static void view_cb_title_changed(WebKitWebView *view, WebKitWebFrame *frame,
+				  const char *title,
+				  struct browser_context *ctx)
+{
+	wpa_printf(MSG_DEBUG, "BROWSER:%s title=%s", __func__, title);
+	os_free(ctx->title);
+	ctx->title = os_strdup(title);
+	browser_update_title(ctx);
+}
+
+
+int hs20_web_browser(const char *url)
+{
+	GtkWidget *scroll;
+	SoupSession *s;
+	WebKitWebView *view;
+	WebKitWebSettings *settings;
+	struct browser_context ctx;
+
+	memset(&ctx, 0, sizeof(ctx));
+	if (!gtk_init_check(NULL, NULL))
+		return -1;
+
+	s = webkit_get_default_session();
+	g_object_set(G_OBJECT(s), "ssl-ca-file",
+		     "/etc/ssl/certs/ca-certificates.crt", NULL);
+	g_object_set(G_OBJECT(s), "ssl-strict", FALSE, NULL);
+
+	ctx.win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+	gtk_window_set_wmclass(GTK_WINDOW(ctx.win), "Hotspot 2.0 client",
+			       "Hotspot 2.0 client");
+	gtk_window_set_default_size(GTK_WINDOW(ctx.win), 800, 600);
+
+	scroll = gtk_scrolled_window_new(NULL, NULL);
+	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
+				       GTK_POLICY_NEVER, GTK_POLICY_NEVER);
+
+	g_signal_connect(G_OBJECT(ctx.win), "destroy",
+			 G_CALLBACK(win_cb_destroy), &ctx);
+
+	view = WEBKIT_WEB_VIEW(webkit_web_view_new());
+	g_signal_connect(G_OBJECT(view), "notify::progress",
+			 G_CALLBACK(view_cb_notify_progress), &ctx);
+	g_signal_connect(G_OBJECT(view), "notify::load-status",
+			 G_CALLBACK(view_cb_notify_load_status), &ctx);
+	g_signal_connect(G_OBJECT(view), "resource-request-starting",
+			 G_CALLBACK(view_cb_resource_request_starting), &ctx);
+	g_signal_connect(G_OBJECT(view), "mime-type-policy-decision-requested",
+			 G_CALLBACK(view_cb_mime_type_policy_decision), &ctx);
+	g_signal_connect(G_OBJECT(view), "download-requested",
+			 G_CALLBACK(view_cb_download_requested), &ctx);
+	g_signal_connect(G_OBJECT(view), "hovering-over-link",
+			 G_CALLBACK(view_cb_hovering_over_link), &ctx);
+	g_signal_connect(G_OBJECT(view), "title-changed",
+			 G_CALLBACK(view_cb_title_changed), &ctx);
+
+	gtk_container_add(GTK_CONTAINER(scroll), GTK_WIDGET(view));
+	gtk_container_add(GTK_CONTAINER(ctx.win), GTK_WIDGET(scroll));
+
+	gtk_widget_grab_focus(GTK_WIDGET(view));
+	gtk_widget_show_all(ctx.win);
+
+	settings = webkit_web_view_get_settings(view);
+	g_object_set(G_OBJECT(settings), "user-agent",
+		     "Mozilla/5.0 (X11; U; Unix; en-US) "
+		     "AppleWebKit/537.15 (KHTML, like Gecko) "
+		     "hs20-client/1.0", NULL);
+	g_object_set(G_OBJECT(settings), "auto-load-images", TRUE, NULL);
+
+	webkit_web_view_load_uri(view, url);
+
+	gtk_main();
+	gtk_widget_destroy(ctx.win);
+	while (gtk_events_pending())
+		gtk_main_iteration();
+
+	free(ctx.hover_link);
+	free(ctx.title);
+	return ctx.success;
+}
diff --git a/src/utils/browser.h b/src/utils/browser.h
new file mode 100644
index 0000000..aaa0eed
--- /dev/null
+++ b/src/utils/browser.h
@@ -0,0 +1,21 @@
+/*
+ * Hotspot 2.0 client - Web browser
+ * Copyright (c) 2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#ifndef BROWSER_H
+#define BROWSER_H
+
+#ifdef CONFIG_NO_BROWSER
+static inline int hs20_web_browser(const char *url)
+{
+	return -1;
+}
+#else /* CONFIG_NO_BROWSER */
+int hs20_web_browser(const char *url);
+#endif /* CONFIG_NO_BROWSER */
+
+#endif /* BROWSER_H */
diff --git a/src/utils/http-utils.h b/src/utils/http-utils.h
new file mode 100644
index 0000000..8d4399a
--- /dev/null
+++ b/src/utils/http-utils.h
@@ -0,0 +1,63 @@
+/*
+ * HTTP wrapper
+ * Copyright (c) 2012-2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#ifndef HTTP_UTILS_H
+#define HTTP_UTILS_H
+
+struct http_ctx;
+
+struct http_othername {
+	char *oid;
+	u8 *data;
+	size_t len;
+};
+
+#define HTTP_MAX_CERT_LOGO_HASH 32
+
+struct http_logo {
+	char *alg_oid;
+	u8 *hash;
+	size_t hash_len;
+	char *uri;
+};
+
+struct http_cert {
+	char **dnsname;
+	unsigned int num_dnsname;
+	struct http_othername *othername;
+	unsigned int num_othername;
+	struct http_logo *logo;
+	unsigned int num_logo;
+};
+
+int soap_init_client(struct http_ctx *ctx, const char *address,
+		     const char *ca_fname, const char *username,
+		     const char *password, const char *client_cert,
+		     const char *client_key);
+int soap_reinit_client(struct http_ctx *ctx);
+xml_node_t * soap_send_receive(struct http_ctx *ctx, xml_node_t *node);
+
+struct http_ctx * http_init_ctx(void *upper_ctx, struct xml_node_ctx *xml_ctx);
+void http_ocsp_set(struct http_ctx *ctx, int val);
+void http_deinit_ctx(struct http_ctx *ctx);
+
+int http_download_file(struct http_ctx *ctx, const char *url,
+		       const char *fname, const char *ca_fname);
+char * http_post(struct http_ctx *ctx, const char *url, const char *data,
+		 const char *content_type, const char *ext_hdr,
+		 const char *ca_fname,
+		 const char *username, const char *password,
+		 const char *client_cert, const char *client_key,
+		 size_t *resp_len);
+void http_set_cert_cb(struct http_ctx *ctx,
+		      int (*cb)(void *ctx, struct http_cert *cert),
+		      void *cb_ctx);
+const char * http_get_err(struct http_ctx *ctx);
+void http_parse_x509_certificate(struct http_ctx *ctx, const char *fname);
+
+#endif /* HTTP_UTILS_H */
diff --git a/src/utils/http_curl.c b/src/utils/http_curl.c
new file mode 100644
index 0000000..668c1a6
--- /dev/null
+++ b/src/utils/http_curl.c
@@ -0,0 +1,1642 @@
+/*
+ * HTTP wrapper for libcurl
+ * Copyright (c) 2012-2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+#include <curl/curl.h>
+#ifdef EAP_TLS_OPENSSL
+#include <openssl/ssl.h>
+#include <openssl/asn1.h>
+#include <openssl/asn1t.h>
+#include <openssl/x509v3.h>
+
+#ifdef SSL_set_tlsext_status_type
+#ifndef OPENSSL_NO_TLSEXT
+#define HAVE_OCSP
+#include <openssl/err.h>
+#include <openssl/ocsp.h>
+#endif /* OPENSSL_NO_TLSEXT */
+#endif /* SSL_set_tlsext_status_type */
+#endif /* EAP_TLS_OPENSSL */
+
+#include "common.h"
+#include "xml-utils.h"
+#include "http-utils.h"
+
+
+struct http_ctx {
+	void *ctx;
+	struct xml_node_ctx *xml;
+	CURL *curl;
+	struct curl_slist *curl_hdr;
+	char *svc_address;
+	char *svc_ca_fname;
+	char *svc_username;
+	char *svc_password;
+	char *svc_client_cert;
+	char *svc_client_key;
+	char *curl_buf;
+	size_t curl_buf_len;
+
+	int (*cert_cb)(void *ctx, struct http_cert *cert);
+	void *cert_cb_ctx;
+
+	enum {
+		NO_OCSP, OPTIONAL_OCSP, MANDATORY_OCSP
+	} ocsp;
+	X509 *peer_cert;
+	X509 *peer_issuer;
+	X509 *peer_issuer_issuer;
+
+	const char *last_err;
+};
+
+
+static void clear_curl(struct http_ctx *ctx)
+{
+	if (ctx->curl) {
+		curl_easy_cleanup(ctx->curl);
+		ctx->curl = NULL;
+	}
+	if (ctx->curl_hdr) {
+		curl_slist_free_all(ctx->curl_hdr);
+		ctx->curl_hdr = NULL;
+	}
+}
+
+
+static void clone_str(char **dst, const char *src)
+{
+	os_free(*dst);
+	if (src)
+		*dst = os_strdup(src);
+	else
+		*dst = NULL;
+}
+
+
+static void debug_dump(struct http_ctx *ctx, const char *title,
+		       const char *buf, size_t len)
+{
+	char *txt = os_malloc(len + 1);
+	if (txt == NULL)
+		return;
+	os_memcpy(txt, buf, len);
+	txt[len] = '\0';
+	while (len > 0) {
+		len--;
+		if (txt[len] == '\r' || txt[len] == '\n')
+		    txt[len] = '\0';
+	}
+	wpa_printf(MSG_MSGDUMP, "%s[%s]", title, txt);
+	os_free(txt);
+}
+
+
+static int curl_cb_debug(CURL *curl, curl_infotype info, char *buf, size_t len,
+			 void *userdata)
+{
+	struct http_ctx *ctx = userdata;
+	switch (info) {
+	case CURLINFO_TEXT:
+		debug_dump(ctx, "CURLINFO_TEXT", buf, len);
+		break;
+	case CURLINFO_HEADER_IN:
+		debug_dump(ctx, "CURLINFO_HEADER_IN", buf, len);
+		break;
+	case CURLINFO_HEADER_OUT:
+		debug_dump(ctx, "CURLINFO_HEADER_OUT", buf, len);
+		break;
+	case CURLINFO_DATA_IN:
+		debug_dump(ctx, "CURLINFO_DATA_IN", buf, len);
+		break;
+	case CURLINFO_DATA_OUT:
+		debug_dump(ctx, "CURLINFO_DATA_OUT", buf, len);
+		break;
+	case CURLINFO_SSL_DATA_IN:
+		wpa_printf(MSG_DEBUG, "debug - CURLINFO_SSL_DATA_IN - %d",
+			   (int) len);
+		break;
+	case CURLINFO_SSL_DATA_OUT:
+		wpa_printf(MSG_DEBUG, "debug - CURLINFO_SSL_DATA_OUT - %d",
+			   (int) len);
+		break;
+	case CURLINFO_END:
+		wpa_printf(MSG_DEBUG, "debug - CURLINFO_END - %d",
+			   (int) len);
+		break;
+	}
+	return 0;
+}
+
+
+static size_t curl_cb_header(void *ptr, size_t size, size_t nmemb,
+			     void *userdata)
+{
+	struct http_ctx *ctx = userdata;
+	debug_dump(ctx, "curl header", ptr, size * nmemb);
+	return size * nmemb;
+}
+
+
+static size_t curl_cb_write(void *ptr, size_t size, size_t nmemb,
+			    void *userdata)
+{
+	struct http_ctx *ctx = userdata;
+	char *n;
+	debug_dump(ctx, "curl write", ptr, size * nmemb);
+	n = os_realloc(ctx->curl_buf, ctx->curl_buf_len + size * nmemb + 1);
+	if (n == NULL)
+		return 0;
+	ctx->curl_buf = n;
+	os_memcpy(n + ctx->curl_buf_len, ptr, size * nmemb);
+	n[ctx->curl_buf_len + size * nmemb] = '\0';
+	ctx->curl_buf_len += size * nmemb;
+	return size * nmemb;
+}
+
+
+#ifdef EAP_TLS_OPENSSL
+
+static void debug_dump_cert(const char *title, X509 *cert)
+{
+	BIO *out;
+	char *txt;
+	size_t rlen;
+
+	out = BIO_new(BIO_s_mem());
+	if (!out)
+		return;
+
+	X509_print_ex(out, cert, XN_FLAG_COMPAT, X509_FLAG_COMPAT);
+	rlen = BIO_ctrl_pending(out);
+	txt = os_malloc(rlen + 1);
+	if (txt) {
+		int res = BIO_read(out, txt, rlen);
+		if (res > 0) {
+			txt[res] = '\0';
+			wpa_printf(MSG_MSGDUMP, "%s:\n%s", title, txt);
+		}
+		os_free(txt);
+	}
+	BIO_free(out);
+}
+
+
+static void add_alt_name_othername(struct http_ctx *ctx, struct http_cert *cert,
+				   OTHERNAME *o)
+{
+	char txt[100];
+	int res;
+	struct http_othername *on;
+	ASN1_TYPE *val;
+
+	on = os_realloc_array(cert->othername, cert->num_othername + 1,
+			      sizeof(struct http_othername));
+	if (on == NULL)
+		return;
+	cert->othername = on;
+	on = &on[cert->num_othername];
+	os_memset(on, 0, sizeof(*on));
+
+	res = OBJ_obj2txt(txt, sizeof(txt), o->type_id, 1);
+	if (res < 0 || res >= (int) sizeof(txt))
+		return;
+
+	on->oid = os_strdup(txt);
+	if (on->oid == NULL)
+		return;
+
+	val = o->value;
+	on->data = val->value.octet_string->data;
+	on->len = val->value.octet_string->length;
+
+	cert->num_othername++;
+}
+
+
+static void add_alt_name_dns(struct http_ctx *ctx, struct http_cert *cert,
+			     ASN1_STRING *name)
+{
+	char *buf;
+	char **n;
+
+	buf = NULL;
+	if (ASN1_STRING_to_UTF8((unsigned char **) &buf, name) < 0)
+		return;
+
+	n = os_realloc_array(cert->dnsname, cert->num_dnsname + 1,
+			     sizeof(char *));
+	if (n == NULL)
+		return;
+
+	cert->dnsname = n;
+	n[cert->num_dnsname] = buf;
+	cert->num_dnsname++;
+}
+
+
+static void add_alt_name(struct http_ctx *ctx, struct http_cert *cert,
+			 const GENERAL_NAME *name)
+{
+	switch (name->type) {
+	case GEN_OTHERNAME:
+		add_alt_name_othername(ctx, cert, name->d.otherName);
+		break;
+	case GEN_DNS:
+		add_alt_name_dns(ctx, cert, name->d.dNSName);
+		break;
+	}
+}
+
+
+static void add_alt_names(struct http_ctx *ctx, struct http_cert *cert,
+			  GENERAL_NAMES *names)
+{
+	int num, i;
+
+	num = sk_GENERAL_NAME_num(names);
+	for (i = 0; i < num; i++) {
+		const GENERAL_NAME *name;
+		name = sk_GENERAL_NAME_value(names, i);
+		add_alt_name(ctx, cert, name);
+	}
+}
+
+
+/* RFC 3709 */
+
+typedef struct {
+	X509_ALGOR *hashAlg;
+	ASN1_OCTET_STRING *hashValue;
+} HashAlgAndValue;
+
+typedef struct {
+	STACK_OF(HashAlgAndValue) *refStructHash;
+	STACK_OF(ASN1_IA5STRING) *refStructURI;
+} LogotypeReference;
+
+typedef struct {
+	ASN1_IA5STRING *mediaType;
+	STACK_OF(HashAlgAndValue) *logotypeHash;
+	STACK_OF(ASN1_IA5STRING) *logotypeURI;
+} LogotypeDetails;
+
+typedef struct {
+	int type;
+	union {
+		ASN1_INTEGER *numBits;
+		ASN1_INTEGER *tableSize;
+	} d;
+} LogotypeImageResolution;
+
+typedef struct {
+	ASN1_INTEGER *type; /* LogotypeImageType ::= INTEGER */
+	ASN1_INTEGER *fileSize;
+	ASN1_INTEGER *xSize;
+	ASN1_INTEGER *ySize;
+	LogotypeImageResolution *resolution;
+	ASN1_IA5STRING *language;
+} LogotypeImageInfo;
+
+typedef struct {
+	LogotypeDetails *imageDetails;
+	LogotypeImageInfo *imageInfo;
+} LogotypeImage;
+
+typedef struct {
+	ASN1_INTEGER *fileSize;
+	ASN1_INTEGER *playTime;
+	ASN1_INTEGER *channels;
+	ASN1_INTEGER *sampleRate;
+	ASN1_IA5STRING *language;
+} LogotypeAudioInfo;
+
+typedef struct {
+	LogotypeDetails *audioDetails;
+	LogotypeAudioInfo *audioInfo;
+} LogotypeAudio;
+
+typedef struct {
+	STACK_OF(LogotypeImage) *image;
+	STACK_OF(LogotypeAudio) *audio;
+} LogotypeData;
+
+typedef struct {
+	int type;
+	union {
+		LogotypeData *direct;
+		LogotypeReference *indirect;
+	} d;
+} LogotypeInfo;
+
+typedef struct {
+	ASN1_OBJECT *logotypeType;
+	LogotypeInfo *info;
+} OtherLogotypeInfo;
+
+typedef struct {
+	STACK_OF(LogotypeInfo) *communityLogos;
+	LogotypeInfo *issuerLogo;
+	LogotypeInfo *subjectLogo;
+	STACK_OF(OtherLogotypeInfo) *otherLogos;
+} LogotypeExtn;
+
+ASN1_SEQUENCE(HashAlgAndValue) = {
+	ASN1_SIMPLE(HashAlgAndValue, hashAlg, X509_ALGOR),
+	ASN1_SIMPLE(HashAlgAndValue, hashValue, ASN1_OCTET_STRING)
+} ASN1_SEQUENCE_END(HashAlgAndValue);
+
+ASN1_SEQUENCE(LogotypeReference) = {
+	ASN1_SEQUENCE_OF(LogotypeReference, refStructHash, HashAlgAndValue),
+	ASN1_SEQUENCE_OF(LogotypeReference, refStructURI, ASN1_IA5STRING)
+} ASN1_SEQUENCE_END(LogotypeReference);
+
+ASN1_SEQUENCE(LogotypeDetails) = {
+	ASN1_SIMPLE(LogotypeDetails, mediaType, ASN1_IA5STRING),
+	ASN1_SEQUENCE_OF(LogotypeDetails, logotypeHash, HashAlgAndValue),
+	ASN1_SEQUENCE_OF(LogotypeDetails, logotypeURI, ASN1_IA5STRING)
+} ASN1_SEQUENCE_END(LogotypeDetails);
+
+ASN1_CHOICE(LogotypeImageResolution) = {
+	ASN1_IMP(LogotypeImageResolution, d.numBits, ASN1_INTEGER, 1),
+	ASN1_IMP(LogotypeImageResolution, d.tableSize, ASN1_INTEGER, 2)
+} ASN1_CHOICE_END(LogotypeImageResolution);
+
+ASN1_SEQUENCE(LogotypeImageInfo) = {
+	ASN1_IMP_OPT(LogotypeImageInfo, type, ASN1_INTEGER, 0),
+	ASN1_SIMPLE(LogotypeImageInfo, fileSize, ASN1_INTEGER),
+	ASN1_SIMPLE(LogotypeImageInfo, xSize, ASN1_INTEGER),
+	ASN1_SIMPLE(LogotypeImageInfo, ySize, ASN1_INTEGER),
+	ASN1_OPT(LogotypeImageInfo, resolution, LogotypeImageResolution),
+	ASN1_IMP_OPT(LogotypeImageInfo, language, ASN1_IA5STRING, 4),
+} ASN1_SEQUENCE_END(LogotypeImageInfo);
+
+ASN1_SEQUENCE(LogotypeImage) = {
+	ASN1_SIMPLE(LogotypeImage, imageDetails, LogotypeDetails),
+	ASN1_OPT(LogotypeImage, imageInfo, LogotypeImageInfo)
+} ASN1_SEQUENCE_END(LogotypeImage);
+
+ASN1_SEQUENCE(LogotypeAudioInfo) = {
+	ASN1_SIMPLE(LogotypeAudioInfo, fileSize, ASN1_INTEGER),
+	ASN1_SIMPLE(LogotypeAudioInfo, playTime, ASN1_INTEGER),
+	ASN1_SIMPLE(LogotypeAudioInfo, channels, ASN1_INTEGER),
+	ASN1_IMP_OPT(LogotypeAudioInfo, sampleRate, ASN1_INTEGER, 3),
+	ASN1_IMP_OPT(LogotypeAudioInfo, language, ASN1_IA5STRING, 4)
+} ASN1_SEQUENCE_END(LogotypeAudioInfo);
+
+ASN1_SEQUENCE(LogotypeAudio) = {
+	ASN1_SIMPLE(LogotypeAudio, audioDetails, LogotypeDetails),
+	ASN1_OPT(LogotypeAudio, audioInfo, LogotypeAudioInfo)
+} ASN1_SEQUENCE_END(LogotypeAudio);
+
+ASN1_SEQUENCE(LogotypeData) = {
+	ASN1_SEQUENCE_OF_OPT(LogotypeData, image, LogotypeImage),
+	ASN1_IMP_SEQUENCE_OF_OPT(LogotypeData, audio, LogotypeAudio, 1)
+} ASN1_SEQUENCE_END(LogotypeData);
+
+ASN1_CHOICE(LogotypeInfo) = {
+	ASN1_IMP(LogotypeInfo, d.direct, LogotypeData, 0),
+	ASN1_IMP(LogotypeInfo, d.indirect, LogotypeReference, 1)
+} ASN1_CHOICE_END(LogotypeInfo);
+
+ASN1_SEQUENCE(OtherLogotypeInfo) = {
+	ASN1_SIMPLE(OtherLogotypeInfo, logotypeType, ASN1_OBJECT),
+	ASN1_SIMPLE(OtherLogotypeInfo, info, LogotypeInfo)
+} ASN1_SEQUENCE_END(OtherLogotypeInfo);
+
+ASN1_SEQUENCE(LogotypeExtn) = {
+	ASN1_EXP_SEQUENCE_OF_OPT(LogotypeExtn, communityLogos, LogotypeInfo, 0),
+	ASN1_EXP_OPT(LogotypeExtn, issuerLogo, LogotypeInfo, 1),
+	ASN1_EXP_OPT(LogotypeExtn, issuerLogo, LogotypeInfo, 2),
+	ASN1_EXP_SEQUENCE_OF_OPT(LogotypeExtn, otherLogos, OtherLogotypeInfo, 3)
+} ASN1_SEQUENCE_END(LogotypeExtn);
+
+IMPLEMENT_ASN1_FUNCTIONS(LogotypeExtn);
+
+#define sk_LogotypeInfo_num(st) SKM_sk_num(LogotypeInfo, (st))
+#define sk_LogotypeInfo_value(st, i) SKM_sk_value(LogotypeInfo, (st), (i))
+#define sk_LogotypeImage_num(st) SKM_sk_num(LogotypeImage, (st))
+#define sk_LogotypeImage_value(st, i) SKM_sk_value(LogotypeImage, (st), (i))
+#define sk_LogotypeAudio_num(st) SKM_sk_num(LogotypeAudio, (st))
+#define sk_LogotypeAudio_value(st, i) SKM_sk_value(LogotypeAudio, (st), (i))
+#define sk_HashAlgAndValue_num(st) SKM_sk_num(HashAlgAndValue, (st))
+#define sk_HashAlgAndValue_value(st, i) SKM_sk_value(HashAlgAndValue, (st), (i))
+#define sk_ASN1_IA5STRING_num(st) SKM_sk_num(ASN1_IA5STRING, (st))
+#define sk_ASN1_IA5STRING_value(st, i) SKM_sk_value(ASN1_IA5STRING, (st), (i))
+
+
+static void add_logo(struct http_ctx *ctx, struct http_cert *hcert,
+		     HashAlgAndValue *hash, ASN1_IA5STRING *uri)
+{
+	char txt[100];
+	int res, len;
+	struct http_logo *n;
+
+	if (hash == NULL || uri == NULL)
+		return;
+
+	res = OBJ_obj2txt(txt, sizeof(txt), hash->hashAlg->algorithm, 1);
+	if (res < 0 || res >= (int) sizeof(txt))
+		return;
+
+	n = os_realloc_array(hcert->logo, hcert->num_logo + 1,
+			     sizeof(struct http_logo));
+	if (n == NULL)
+		return;
+	hcert->logo = n;
+	n = &hcert->logo[hcert->num_logo];
+	os_memset(n, 0, sizeof(*n));
+
+	n->alg_oid = os_strdup(txt);
+	if (n->alg_oid == NULL)
+		return;
+
+	n->hash_len = ASN1_STRING_length(hash->hashValue);
+	n->hash = os_malloc(n->hash_len);
+	if (n->hash == NULL) {
+		os_free(n->alg_oid);
+		return;
+	}
+	os_memcpy(n->hash, ASN1_STRING_data(hash->hashValue), n->hash_len);
+
+	len = ASN1_STRING_length(uri);
+	n->uri = os_malloc(len + 1);
+	if (n->uri == NULL) {
+		os_free(n->alg_oid);
+		os_free(n->hash);
+		return;
+	}
+	os_memcpy(n->uri, ASN1_STRING_data(uri), len);
+	n->uri[len] = '\0';
+
+	hcert->num_logo++;
+}
+
+
+static void add_logo_direct(struct http_ctx *ctx, struct http_cert *hcert,
+			    LogotypeData *data)
+{
+	int i, num;
+
+	if (data->image == NULL)
+		return;
+
+	num = sk_LogotypeImage_num(data->image);
+	for (i = 0; i < num; i++) {
+		LogotypeImage *image;
+		LogotypeDetails *details;
+		int j, hash_num, uri_num;
+		HashAlgAndValue *found_hash = NULL;
+
+		image = sk_LogotypeImage_value(data->image, i);
+		if (image == NULL)
+			continue;
+
+		details = image->imageDetails;
+		if (details == NULL)
+			continue;
+
+		hash_num = sk_HashAlgAndValue_num(details->logotypeHash);
+		for (j = 0; j < hash_num; j++) {
+			HashAlgAndValue *hash;
+			char txt[100];
+			int res;
+			hash = sk_HashAlgAndValue_value(details->logotypeHash,
+							j);
+			if (hash == NULL)
+				continue;
+			res = OBJ_obj2txt(txt, sizeof(txt),
+					  hash->hashAlg->algorithm, 1);
+			if (res < 0 || res >= (int) sizeof(txt))
+				continue;
+			if (os_strcmp(txt, "2.16.840.1.101.3.4.2.1") == 0) {
+				found_hash = hash;
+				break;
+			}
+		}
+
+		if (!found_hash) {
+			wpa_printf(MSG_DEBUG, "OpenSSL: No SHA256 hash found for the logo");
+			continue;
+		}
+
+		uri_num = sk_ASN1_IA5STRING_num(details->logotypeURI);
+		for (j = 0; j < uri_num; j++) {
+			ASN1_IA5STRING *uri;
+			uri = sk_ASN1_IA5STRING_value(details->logotypeURI, j);
+			add_logo(ctx, hcert, found_hash, uri);
+		}
+	}
+}
+
+
+static void add_logo_indirect(struct http_ctx *ctx, struct http_cert *hcert,
+			      LogotypeReference *ref)
+{
+	int j, hash_num, uri_num;
+
+	hash_num = sk_HashAlgAndValue_num(ref->refStructHash);
+	uri_num = sk_ASN1_IA5STRING_num(ref->refStructURI);
+	if (hash_num != uri_num) {
+		wpa_printf(MSG_INFO, "Unexpected LogotypeReference array size difference %d != %d",
+			   hash_num, uri_num);
+		return;
+	}
+
+	for (j = 0; j < hash_num; j++) {
+		HashAlgAndValue *hash;
+		ASN1_IA5STRING *uri;
+		hash = sk_HashAlgAndValue_value(ref->refStructHash, j);
+		uri = sk_ASN1_IA5STRING_value(ref->refStructURI, j);
+		add_logo(ctx, hcert, hash, uri);
+	}
+}
+
+
+static void i2r_HashAlgAndValue(HashAlgAndValue *hash, BIO *out, int indent)
+{
+	int i;
+	const unsigned char *data;
+
+	BIO_printf(out, "%*shashAlg: ", indent, "");
+	i2a_ASN1_OBJECT(out, hash->hashAlg->algorithm);
+	BIO_printf(out, "\n");
+
+	BIO_printf(out, "%*shashValue: ", indent, "");
+	data = hash->hashValue->data;
+	for (i = 0; i < hash->hashValue->length; i++)
+		BIO_printf(out, "%s%02x", i > 0 ? ":" : "", data[i]);
+	BIO_printf(out, "\n");
+}
+
+static void i2r_LogotypeDetails(LogotypeDetails *details, BIO *out, int indent)
+{
+	int i, num;
+
+	BIO_printf(out, "%*sLogotypeDetails\n", indent, "");
+	if (details->mediaType) {
+		BIO_printf(out, "%*smediaType: ", indent, "");
+		ASN1_STRING_print(out, details->mediaType);
+		BIO_printf(out, "\n");
+	}
+
+	num = details->logotypeHash ?
+		sk_HashAlgAndValue_num(details->logotypeHash) : 0;
+	for (i = 0; i < num; i++) {
+		HashAlgAndValue *hash;
+		hash = sk_HashAlgAndValue_value(details->logotypeHash, i);
+		i2r_HashAlgAndValue(hash, out, indent);
+	}
+
+	num = details->logotypeURI ?
+		sk_ASN1_IA5STRING_num(details->logotypeURI) : 0;
+	for (i = 0; i < num; i++) {
+		ASN1_IA5STRING *uri;
+		uri = sk_ASN1_IA5STRING_value(details->logotypeURI, i);
+		BIO_printf(out, "%*slogotypeURI: ", indent, "");
+		ASN1_STRING_print(out, uri);
+		BIO_printf(out, "\n");
+	}
+}
+
+static void i2r_LogotypeImageInfo(LogotypeImageInfo *info, BIO *out, int indent)
+{
+	long val;
+
+	BIO_printf(out, "%*sLogotypeImageInfo\n", indent, "");
+	if (info->type) {
+		val = ASN1_INTEGER_get(info->type);
+		BIO_printf(out, "%*stype: %ld\n", indent, "", val);
+	} else {
+		BIO_printf(out, "%*stype: default (1)\n", indent, "");
+	}
+	val = ASN1_INTEGER_get(info->xSize);
+	BIO_printf(out, "%*sxSize: %ld\n", indent, "", val);
+	val = ASN1_INTEGER_get(info->ySize);
+	BIO_printf(out, "%*sySize: %ld\n", indent, "", val);
+	if (info->resolution) {
+		BIO_printf(out, "%*sresolution\n", indent, "");
+		/* TODO */
+	}
+	if (info->language) {
+		BIO_printf(out, "%*slanguage: ", indent, "");
+		ASN1_STRING_print(out, info->language);
+		BIO_printf(out, "\n");
+	}
+}
+
+static void i2r_LogotypeImage(LogotypeImage *image, BIO *out, int indent)
+{
+	BIO_printf(out, "%*sLogotypeImage\n", indent, "");
+	if (image->imageDetails) {
+		i2r_LogotypeDetails(image->imageDetails, out, indent + 4);
+	}
+	if (image->imageInfo) {
+		i2r_LogotypeImageInfo(image->imageInfo, out, indent + 4);
+	}
+}
+
+static void i2r_LogotypeData(LogotypeData *data, const char *title, BIO *out,
+			     int indent)
+{
+	int i, num;
+
+	BIO_printf(out, "%*s%s - LogotypeData\n", indent, "", title);
+
+	num = data->image ? sk_LogotypeImage_num(data->image) : 0;
+	for (i = 0; i < num; i++) {
+		LogotypeImage *image = sk_LogotypeImage_value(data->image, i);
+		i2r_LogotypeImage(image, out, indent + 4);
+	}
+
+	num = data->audio ? sk_LogotypeAudio_num(data->audio) : 0;
+	for (i = 0; i < num; i++) {
+		BIO_printf(out, "%*saudio: TODO\n", indent, "");
+	}
+}
+
+static void i2r_LogotypeReference(LogotypeReference *ref, const char *title,
+				  BIO *out, int indent)
+{
+	int i, hash_num, uri_num;
+
+	BIO_printf(out, "%*s%s - LogotypeReference\n", indent, "", title);
+
+	hash_num = ref->refStructHash ?
+		sk_HashAlgAndValue_num(ref->refStructHash) : 0;
+	uri_num = ref->refStructURI ?
+		sk_ASN1_IA5STRING_num(ref->refStructURI) : 0;
+	if (hash_num != uri_num) {
+		BIO_printf(out, "%*sUnexpected LogotypeReference array size difference %d != %d\n",
+			   indent, "", hash_num, uri_num);
+		return;
+	}
+
+	for (i = 0; i < hash_num; i++) {
+		HashAlgAndValue *hash;
+		ASN1_IA5STRING *uri;
+
+		hash = sk_HashAlgAndValue_value(ref->refStructHash, i);
+		i2r_HashAlgAndValue(hash, out, indent);
+
+		uri = sk_ASN1_IA5STRING_value(ref->refStructURI, i);
+		BIO_printf(out, "%*srefStructURI: ", indent, "");
+		ASN1_STRING_print(out, uri);
+		BIO_printf(out, "\n");
+	}
+}
+
+static void i2r_LogotypeInfo(LogotypeInfo *info, const char *title, BIO *out,
+			     int indent)
+{
+	switch (info->type) {
+	case 0:
+		i2r_LogotypeData(info->d.direct, title, out, indent);
+		break;
+	case 1:
+		i2r_LogotypeReference(info->d.indirect, title, out, indent);
+		break;
+	}
+}
+
+static void debug_print_logotypeext(LogotypeExtn *logo)
+{
+	BIO *out;
+	int i, num;
+	int indent = 0;
+
+	out = BIO_new_fp(stdout, BIO_NOCLOSE);
+	if (out == NULL)
+		return;
+
+	if (logo->communityLogos) {
+		num = sk_LogotypeInfo_num(logo->communityLogos);
+		for (i = 0; i < num; i++) {
+			LogotypeInfo *info;
+			info = sk_LogotypeInfo_value(logo->communityLogos, i);
+			i2r_LogotypeInfo(info, "communityLogo", out, indent);
+		}
+	}
+
+	if (logo->issuerLogo) {
+		i2r_LogotypeInfo(logo->issuerLogo, "issuerLogo", out, indent );
+	}
+
+	if (logo->subjectLogo) {
+		i2r_LogotypeInfo(logo->subjectLogo, "subjectLogo", out, indent);
+	}
+
+	if (logo->otherLogos) {
+		BIO_printf(out, "%*sotherLogos - TODO\n", indent, "");
+	}
+
+	BIO_free(out);
+}
+
+
+static void add_logotype_ext(struct http_ctx *ctx, struct http_cert *hcert,
+			     X509 *cert)
+{
+	ASN1_OBJECT *obj;
+	int pos;
+	X509_EXTENSION *ext;
+	ASN1_OCTET_STRING *os;
+	LogotypeExtn *logo;
+	const unsigned char *data;
+	int i, num;
+
+	obj = OBJ_txt2obj("1.3.6.1.5.5.7.1.12", 0);
+	if (obj == NULL)
+		return;
+
+	pos = X509_get_ext_by_OBJ(cert, obj, -1);
+	if (pos < 0) {
+		wpa_printf(MSG_INFO, "No logotype extension included");
+		return;
+	}
+
+	wpa_printf(MSG_INFO, "Parsing logotype extension");
+	ext = X509_get_ext(cert, pos);
+	if (!ext) {
+		wpa_printf(MSG_INFO, "Could not get logotype extension");
+		return;
+	}
+
+	os = X509_EXTENSION_get_data(ext);
+	if (os == NULL) {
+		wpa_printf(MSG_INFO, "Could not get logotype extension data");
+		return;
+	}
+
+	wpa_hexdump(MSG_DEBUG, "logotypeExtn",
+		    ASN1_STRING_data(os), ASN1_STRING_length(os));
+
+	data = ASN1_STRING_data(os);
+	logo = d2i_LogotypeExtn(NULL, &data, ASN1_STRING_length(os));
+	if (logo == NULL) {
+		wpa_printf(MSG_INFO, "Failed to parse logotypeExtn");
+		return;
+	}
+
+	if (wpa_debug_level < MSG_INFO)
+		debug_print_logotypeext(logo);
+
+	if (!logo->communityLogos) {
+		wpa_printf(MSG_INFO, "No communityLogos included");
+		LogotypeExtn_free(logo);
+		return;
+	}
+
+	num = sk_LogotypeInfo_num(logo->communityLogos);
+	for (i = 0; i < num; i++) {
+		LogotypeInfo *info;
+		info = sk_LogotypeInfo_value(logo->communityLogos, i);
+		switch (info->type) {
+		case 0:
+			add_logo_direct(ctx, hcert, info->d.direct);
+			break;
+		case 1:
+			add_logo_indirect(ctx, hcert, info->d.indirect);
+			break;
+		}
+	}
+
+	LogotypeExtn_free(logo);
+}
+
+
+static void parse_cert(struct http_ctx *ctx, struct http_cert *hcert,
+		       X509 *cert, GENERAL_NAMES **names)
+{
+	os_memset(hcert, 0, sizeof(*hcert));
+
+	*names = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
+	if (*names)
+		add_alt_names(ctx, hcert, *names);
+
+	add_logotype_ext(ctx, hcert, cert);
+}
+
+
+static void parse_cert_free(struct http_cert *hcert, GENERAL_NAMES *names)
+{
+	unsigned int i;
+
+	for (i = 0; i < hcert->num_dnsname; i++)
+		OPENSSL_free(hcert->dnsname[i]);
+	os_free(hcert->dnsname);
+
+	for (i = 0; i < hcert->num_othername; i++)
+		os_free(hcert->othername[i].oid);
+	os_free(hcert->othername);
+
+	for (i = 0; i < hcert->num_logo; i++) {
+		os_free(hcert->logo[i].alg_oid);
+		os_free(hcert->logo[i].hash);
+		os_free(hcert->logo[i].uri);
+	}
+	os_free(hcert->logo);
+
+	sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free);
+}
+
+
+static int validate_server_cert(struct http_ctx *ctx, X509 *cert)
+{
+	GENERAL_NAMES *names;
+	struct http_cert hcert;
+	int ret;
+
+	if (ctx->cert_cb == NULL)
+		return 0;
+
+	if (0) {
+		BIO *out;
+		out = BIO_new_fp(stdout, BIO_NOCLOSE);
+		X509_print_ex(out, cert, XN_FLAG_COMPAT, X509_FLAG_COMPAT);
+		BIO_free(out);
+	}
+
+	parse_cert(ctx, &hcert, cert, &names);
+	ret = ctx->cert_cb(ctx->cert_cb_ctx, &hcert);
+	parse_cert_free(&hcert, names);
+
+	return ret;
+}
+
+
+void http_parse_x509_certificate(struct http_ctx *ctx, const char *fname)
+{
+	BIO *in, *out;
+	X509 *cert;
+	GENERAL_NAMES *names;
+	struct http_cert hcert;
+	unsigned int i;
+
+	in = BIO_new_file(fname, "r");
+	if (in == NULL) {
+		wpa_printf(MSG_ERROR, "Could not read '%s'", fname);
+		return;
+	}
+
+	cert = d2i_X509_bio(in, NULL);
+	BIO_free(in);
+
+	if (cert == NULL) {
+		wpa_printf(MSG_ERROR, "Could not parse certificate");
+		return;
+	}
+
+	out = BIO_new_fp(stdout, BIO_NOCLOSE);
+	if (out) {
+		X509_print_ex(out, cert, XN_FLAG_COMPAT,
+			      X509_FLAG_COMPAT);
+		BIO_free(out);
+	}
+
+	wpa_printf(MSG_INFO, "Additional parsing information:");
+	parse_cert(ctx, &hcert, cert, &names);
+	for (i = 0; i < hcert.num_othername; i++) {
+		if (os_strcmp(hcert.othername[i].oid,
+			      "1.3.6.1.4.1.40808.1.1.1") == 0) {
+			char *name = os_zalloc(hcert.othername[i].len + 1);
+			if (name) {
+				os_memcpy(name, hcert.othername[i].data,
+					  hcert.othername[i].len);
+				wpa_printf(MSG_INFO,
+					   "id-wfa-hotspot-friendlyName: %s",
+					   name);
+				os_free(name);
+			}
+			wpa_hexdump_ascii(MSG_INFO,
+					  "id-wfa-hotspot-friendlyName",
+					  hcert.othername[i].data,
+					  hcert.othername[i].len);
+		} else {
+			wpa_printf(MSG_INFO, "subjAltName[othername]: oid=%s",
+				   hcert.othername[i].oid);
+			wpa_hexdump_ascii(MSG_INFO, "unknown othername",
+					  hcert.othername[i].data,
+					  hcert.othername[i].len);
+		}
+	}
+	parse_cert_free(&hcert, names);
+
+	X509_free(cert);
+}
+
+
+static int curl_cb_ssl_verify(int preverify_ok, X509_STORE_CTX *x509_ctx)
+{
+	struct http_ctx *ctx;
+	X509 *cert;
+	int err, depth;
+	char buf[256];
+	X509_NAME *name;
+	const char *err_str;
+	SSL *ssl;
+	SSL_CTX *ssl_ctx;
+
+	ssl = X509_STORE_CTX_get_ex_data(x509_ctx,
+					 SSL_get_ex_data_X509_STORE_CTX_idx());
+	ssl_ctx = ssl->ctx;
+	ctx = SSL_CTX_get_app_data(ssl_ctx);
+
+	wpa_printf(MSG_DEBUG, "curl_cb_ssl_verify");
+
+	err = X509_STORE_CTX_get_error(x509_ctx);
+	err_str = X509_verify_cert_error_string(err);
+	depth = X509_STORE_CTX_get_error_depth(x509_ctx);
+	cert = X509_STORE_CTX_get_current_cert(x509_ctx);
+	if (!cert) {
+		wpa_printf(MSG_INFO, "No server certificate available");
+		ctx->last_err = "No server certificate available";
+		return 0;
+	}
+
+	if (depth == 0)
+		ctx->peer_cert = cert;
+	else if (depth == 1)
+		ctx->peer_issuer = cert;
+	else if (depth == 2)
+		ctx->peer_issuer_issuer = cert;
+
+	name = X509_get_subject_name(cert);
+	X509_NAME_oneline(name, buf, sizeof(buf));
+	wpa_printf(MSG_INFO, "Server certificate chain - depth=%d err=%d (%s) subject=%s",
+		   depth, err, err_str, buf);
+	debug_dump_cert("Server certificate chain - certificate", cert);
+
+	if (depth == 0 && preverify_ok && validate_server_cert(ctx, cert) < 0)
+		return 0;
+
+	if (!preverify_ok)
+		ctx->last_err = "TLS validation failed";
+
+	return preverify_ok;
+}
+
+
+#ifdef HAVE_OCSP
+
+static void ocsp_debug_print_resp(OCSP_RESPONSE *rsp)
+{
+	BIO *out;
+	size_t rlen;
+	char *txt;
+	int res;
+
+	out = BIO_new(BIO_s_mem());
+	if (!out)
+		return;
+
+	OCSP_RESPONSE_print(out, rsp, 0);
+	rlen = BIO_ctrl_pending(out);
+	txt = os_malloc(rlen + 1);
+	if (!txt) {
+		BIO_free(out);
+		return;
+	}
+
+	res = BIO_read(out, txt, rlen);
+	if (res > 0) {
+		txt[res] = '\0';
+		wpa_printf(MSG_MSGDUMP, "OpenSSL: OCSP Response\n%s", txt);
+	}
+	os_free(txt);
+	BIO_free(out);
+}
+
+
+static void tls_show_errors(const char *func, const char *txt)
+{
+	unsigned long err;
+
+	wpa_printf(MSG_DEBUG, "OpenSSL: %s - %s %s",
+		   func, txt, ERR_error_string(ERR_get_error(), NULL));
+
+	while ((err = ERR_get_error())) {
+		wpa_printf(MSG_DEBUG, "OpenSSL: pending error: %s",
+			   ERR_error_string(err, NULL));
+	}
+}
+
+
+static int ocsp_resp_cb(SSL *s, void *arg)
+{
+	struct http_ctx *ctx = arg;
+	const unsigned char *p;
+	int len, status, reason;
+	OCSP_RESPONSE *rsp;
+	OCSP_BASICRESP *basic;
+	OCSP_CERTID *id;
+	ASN1_GENERALIZEDTIME *produced_at, *this_update, *next_update;
+	X509_STORE *store;
+	STACK_OF(X509) *certs = NULL;
+
+	len = SSL_get_tlsext_status_ocsp_resp(s, &p);
+	if (!p) {
+		wpa_printf(MSG_DEBUG, "OpenSSL: No OCSP response received");
+		if (ctx->ocsp == MANDATORY_OCSP)
+			ctx->last_err = "No OCSP response received";
+		return (ctx->ocsp == MANDATORY_OCSP) ? 0 : 1;
+	}
+
+	wpa_hexdump(MSG_DEBUG, "OpenSSL: OCSP response", p, len);
+
+	rsp = d2i_OCSP_RESPONSE(NULL, &p, len);
+	if (!rsp) {
+		wpa_printf(MSG_INFO, "OpenSSL: Failed to parse OCSP response");
+		ctx->last_err = "Failed to parse OCSP response";
+		return 0;
+	}
+
+	ocsp_debug_print_resp(rsp);
+
+	status = OCSP_response_status(rsp);
+	if (status != OCSP_RESPONSE_STATUS_SUCCESSFUL) {
+		wpa_printf(MSG_INFO, "OpenSSL: OCSP responder error %d (%s)",
+			   status, OCSP_response_status_str(status));
+		ctx->last_err = "OCSP responder error";
+		return 0;
+	}
+
+	basic = OCSP_response_get1_basic(rsp);
+	if (!basic) {
+		wpa_printf(MSG_INFO, "OpenSSL: Could not find BasicOCSPResponse");
+		ctx->last_err = "Could not find BasicOCSPResponse";
+		return 0;
+	}
+
+	store = SSL_CTX_get_cert_store(s->ctx);
+	if (ctx->peer_issuer) {
+		wpa_printf(MSG_DEBUG, "OpenSSL: Add issuer");
+		debug_dump_cert("OpenSSL: Issuer certificate",
+				ctx->peer_issuer);
+
+		if (X509_STORE_add_cert(store, ctx->peer_issuer) != 1) {
+			tls_show_errors(__func__,
+					"OpenSSL: Could not add issuer to certificate store\n");
+		}
+		certs = sk_X509_new_null();
+		if (certs) {
+			X509 *cert;
+			cert = X509_dup(ctx->peer_issuer);
+			if (cert && !sk_X509_push(certs, cert)) {
+				tls_show_errors(
+					__func__,
+					"OpenSSL: Could not add issuer to OCSP responder trust store\n");
+				X509_free(cert);
+				sk_X509_free(certs);
+				certs = NULL;
+			}
+			if (ctx->peer_issuer_issuer) {
+				X509 *cert;
+				cert = X509_dup(ctx->peer_issuer_issuer);
+				if (cert && !sk_X509_push(certs, cert)) {
+					tls_show_errors(
+						__func__,
+						"OpenSSL: Could not add issuer to OCSP responder trust store\n");
+					X509_free(cert);
+				}
+			}
+		}
+	}
+
+	status = OCSP_basic_verify(basic, certs, store, OCSP_TRUSTOTHER);
+	sk_X509_pop_free(certs, X509_free);
+	if (status <= 0) {
+		tls_show_errors(__func__,
+				"OpenSSL: OCSP response failed verification");
+		OCSP_BASICRESP_free(basic);
+		OCSP_RESPONSE_free(rsp);
+		ctx->last_err = "OCSP response failed verification";
+		return 0;
+	}
+
+	wpa_printf(MSG_DEBUG, "OpenSSL: OCSP response verification succeeded");
+
+	if (!ctx->peer_cert) {
+		wpa_printf(MSG_DEBUG, "OpenSSL: Peer certificate not available for OCSP status check");
+		OCSP_BASICRESP_free(basic);
+		OCSP_RESPONSE_free(rsp);
+		ctx->last_err = "Peer certificate not available for OCSP status check";
+		return 0;
+	}
+
+	if (!ctx->peer_issuer) {
+		wpa_printf(MSG_DEBUG, "OpenSSL: Peer issuer certificate not available for OCSP status check");
+		OCSP_BASICRESP_free(basic);
+		OCSP_RESPONSE_free(rsp);
+		ctx->last_err = "Peer issuer certificate not available for OCSP status check";
+		return 0;
+	}
+
+	id = OCSP_cert_to_id(NULL, ctx->peer_cert, ctx->peer_issuer);
+	if (!id) {
+		wpa_printf(MSG_DEBUG, "OpenSSL: Could not create OCSP certificate identifier");
+		OCSP_BASICRESP_free(basic);
+		OCSP_RESPONSE_free(rsp);
+		ctx->last_err = "Could not create OCSP certificate identifier";
+		return 0;
+	}
+
+	if (!OCSP_resp_find_status(basic, id, &status, &reason, &produced_at,
+				   &this_update, &next_update)) {
+		wpa_printf(MSG_INFO, "OpenSSL: Could not find current server certificate from OCSP response%s",
+			   (ctx->ocsp == MANDATORY_OCSP) ? "" :
+			   " (OCSP not required)");
+		OCSP_BASICRESP_free(basic);
+		OCSP_RESPONSE_free(rsp);
+		if (ctx->ocsp == MANDATORY_OCSP)
+
+			ctx->last_err = "Could not find current server certificate from OCSP response";
+		return (ctx->ocsp == MANDATORY_OCSP) ? 0 : 1;
+	}
+
+	if (!OCSP_check_validity(this_update, next_update, 5 * 60, -1)) {
+		tls_show_errors(__func__, "OpenSSL: OCSP status times invalid");
+		OCSP_BASICRESP_free(basic);
+		OCSP_RESPONSE_free(rsp);
+		ctx->last_err = "OCSP status times invalid";
+		return 0;
+	}
+
+	OCSP_BASICRESP_free(basic);
+	OCSP_RESPONSE_free(rsp);
+
+	wpa_printf(MSG_DEBUG, "OpenSSL: OCSP status for server certificate: %s",
+		   OCSP_cert_status_str(status));
+
+	if (status == V_OCSP_CERTSTATUS_GOOD)
+		return 1;
+	if (status == V_OCSP_CERTSTATUS_REVOKED)
+		ctx->last_err = "Server certificate has been revoked";
+		return 0;
+	if (ctx->ocsp == MANDATORY_OCSP) {
+		wpa_printf(MSG_DEBUG, "OpenSSL: OCSP status unknown, but OCSP required");
+		ctx->last_err = "OCSP status unknown";
+		return 0;
+	}
+	wpa_printf(MSG_DEBUG, "OpenSSL: OCSP status unknown, but OCSP was not required, so allow connection to continue");
+	return 1;
+}
+
+
+static SSL_METHOD patch_ssl_method;
+static const SSL_METHOD *real_ssl_method;
+
+static int curl_patch_ssl_new(SSL *s)
+{
+	SSL_CTX *ssl = s->ctx;
+	int ret;
+
+	ssl->method = real_ssl_method;
+	s->method = real_ssl_method;
+
+	ret = s->method->ssl_new(s);
+	SSL_set_tlsext_status_type(s, TLSEXT_STATUSTYPE_ocsp);
+
+	return ret;
+}
+
+#endif /* HAVE_OCSP */
+
+
+static CURLcode curl_cb_ssl(CURL *curl, void *sslctx, void *parm)
+{
+	struct http_ctx *ctx = parm;
+	SSL_CTX *ssl = sslctx;
+
+	wpa_printf(MSG_DEBUG, "curl_cb_ssl");
+	SSL_CTX_set_app_data(ssl, ctx);
+	SSL_CTX_set_verify(ssl, SSL_VERIFY_PEER, curl_cb_ssl_verify);
+
+#ifdef HAVE_OCSP
+	if (ctx->ocsp != NO_OCSP) {
+		SSL_CTX_set_tlsext_status_cb(ssl, ocsp_resp_cb);
+		SSL_CTX_set_tlsext_status_arg(ssl, ctx);
+
+		/*
+		 * Use a temporary SSL_METHOD to get a callback on SSL_new()
+		 * from libcurl since there is no proper callback registration
+		 * available for this.
+		 */
+		os_memset(&patch_ssl_method, 0, sizeof(patch_ssl_method));
+		patch_ssl_method.ssl_new = curl_patch_ssl_new;
+		real_ssl_method = ssl->method;
+		ssl->method = &patch_ssl_method;
+	}
+#endif /* HAVE_OCSP */
+
+	return CURLE_OK;
+}
+
+#endif /* EAP_TLS_OPENSSL */
+
+
+static CURL * setup_curl_post(struct http_ctx *ctx, const char *address,
+			      const char *ca_fname, const char *username,
+			      const char *password, const char *client_cert,
+			      const char *client_key)
+{
+	CURL *curl;
+
+	wpa_printf(MSG_DEBUG, "Start HTTP client: address=%s ca_fname=%s "
+		   "username=%s", address, ca_fname, username);
+
+	curl = curl_easy_init();
+	if (curl == NULL)
+		return NULL;
+
+	curl_easy_setopt(curl, CURLOPT_URL, address);
+	curl_easy_setopt(curl, CURLOPT_POST, 1L);
+	if (ca_fname) {
+		curl_easy_setopt(curl, CURLOPT_CAINFO, ca_fname);
+		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
+#ifdef EAP_TLS_OPENSSL
+		curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, curl_cb_ssl);
+		curl_easy_setopt(curl, CURLOPT_SSL_CTX_DATA, ctx);
+#endif /* EAP_TLS_OPENSSL */
+	} else {
+		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
+	}
+	if (client_cert && client_key) {
+		curl_easy_setopt(curl, CURLOPT_SSLCERT, client_cert);
+		curl_easy_setopt(curl, CURLOPT_SSLKEY, client_key);
+	}
+	/* TODO: use curl_easy_getinfo() with CURLINFO_CERTINFO to fetch
+	 * information about the server certificate */
+	curl_easy_setopt(curl, CURLOPT_CERTINFO, 1L);
+	curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, curl_cb_debug);
+	curl_easy_setopt(curl, CURLOPT_DEBUGDATA, ctx);
+	curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_cb_header);
+	curl_easy_setopt(curl, CURLOPT_HEADERDATA, ctx);
+	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_cb_write);
+	curl_easy_setopt(curl, CURLOPT_WRITEDATA, ctx);
+	curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
+	if (username) {
+		curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_ANYSAFE);
+		curl_easy_setopt(curl, CURLOPT_USERNAME, username);
+		curl_easy_setopt(curl, CURLOPT_PASSWORD, password);
+	}
+
+	return curl;
+}
+
+
+static int post_init_client(struct http_ctx *ctx, const char *address,
+			    const char *ca_fname, const char *username,
+			    const char *password, const char *client_cert,
+			    const char *client_key)
+{
+	char *pos;
+	int count;
+
+	clone_str(&ctx->svc_address, address);
+	clone_str(&ctx->svc_ca_fname, ca_fname);
+	clone_str(&ctx->svc_username, username);
+	clone_str(&ctx->svc_password, password);
+	clone_str(&ctx->svc_client_cert, client_cert);
+	clone_str(&ctx->svc_client_key, client_key);
+
+	/*
+	 * Workaround for Apache "Hostname 'FOO' provided via SNI and hostname
+	 * 'foo' provided via HTTP are different.
+	 */
+	for (count = 0, pos = ctx->svc_address; count < 3 && pos && *pos;
+	     pos++) {
+		if (*pos == '/')
+			count++;
+		*pos = tolower(*pos);
+	}
+
+	ctx->curl = setup_curl_post(ctx, ctx->svc_address, ca_fname, username,
+				    password, client_cert, client_key);
+	if (ctx->curl == NULL)
+		return -1;
+
+	return 0;
+}
+
+
+int soap_init_client(struct http_ctx *ctx, const char *address,
+		     const char *ca_fname, const char *username,
+		     const char *password, const char *client_cert,
+		     const char *client_key)
+{
+	if (post_init_client(ctx, address, ca_fname, username, password,
+			     client_cert, client_key) < 0)
+		return -1;
+
+	ctx->curl_hdr = curl_slist_append(ctx->curl_hdr,
+					  "Content-Type: application/soap+xml");
+	ctx->curl_hdr = curl_slist_append(ctx->curl_hdr, "SOAPAction: ");
+	ctx->curl_hdr = curl_slist_append(ctx->curl_hdr, "Expect:");
+	curl_easy_setopt(ctx->curl, CURLOPT_HTTPHEADER, ctx->curl_hdr);
+
+	return 0;
+}
+
+
+int soap_reinit_client(struct http_ctx *ctx)
+{
+	char *address = NULL;
+	char *ca_fname = NULL;
+	char *username = NULL;
+	char *password = NULL;
+	char *client_cert = NULL;
+	char *client_key = NULL;
+	int ret;
+
+	clear_curl(ctx);
+
+	clone_str(&address, ctx->svc_address);
+	clone_str(&ca_fname, ctx->svc_ca_fname);
+	clone_str(&username, ctx->svc_username);
+	clone_str(&password, ctx->svc_password);
+	clone_str(&client_cert, ctx->svc_client_cert);
+	clone_str(&client_key, ctx->svc_client_key);
+
+	ret = soap_init_client(ctx, address, ca_fname, username, password,
+			       client_cert, client_key);
+	os_free(address);
+	os_free(ca_fname);
+	os_free(username);
+	os_free(password);
+	os_free(client_cert);
+	os_free(client_key);
+	return ret;
+}
+
+
+static void free_curl_buf(struct http_ctx *ctx)
+{
+	os_free(ctx->curl_buf);
+	ctx->curl_buf = NULL;
+	ctx->curl_buf_len = 0;
+}
+
+
+xml_node_t * soap_send_receive(struct http_ctx *ctx, xml_node_t *node)
+{
+	char *str;
+	xml_node_t *envelope, *ret, *resp, *n;
+	CURLcode res;
+	long http = 0;
+
+	ctx->last_err = NULL;
+
+	wpa_printf(MSG_DEBUG, "SOAP: Sending message");
+	envelope = soap_build_envelope(ctx->xml, node);
+	str = xml_node_to_str(ctx->xml, envelope);
+	xml_node_free(ctx->xml, envelope);
+	wpa_printf(MSG_MSGDUMP, "SOAP[%s]", str);
+
+	curl_easy_setopt(ctx->curl, CURLOPT_POSTFIELDS, str);
+	free_curl_buf(ctx);
+
+	res = curl_easy_perform(ctx->curl);
+	if (res != CURLE_OK) {
+		if (!ctx->last_err)
+			ctx->last_err = curl_easy_strerror(res);
+		wpa_printf(MSG_ERROR, "curl_easy_perform() failed: %s",
+			   ctx->last_err);
+		os_free(str);
+		free_curl_buf(ctx);
+		return NULL;
+	}
+	os_free(str);
+
+	curl_easy_getinfo(ctx->curl, CURLINFO_RESPONSE_CODE, &http);
+	wpa_printf(MSG_DEBUG, "SOAP: Server response code %ld", http);
+	if (http != 200) {
+		ctx->last_err = "HTTP download failed";
+		wpa_printf(MSG_INFO, "HTTP download failed - code %ld", http);
+		free_curl_buf(ctx);
+		return NULL;
+	}
+
+	if (ctx->curl_buf == NULL)
+		return NULL;
+
+	wpa_printf(MSG_MSGDUMP, "Server response:\n%s", ctx->curl_buf);
+	resp = xml_node_from_buf(ctx->xml, ctx->curl_buf);
+	free_curl_buf(ctx);
+	if (resp == NULL) {
+		wpa_printf(MSG_INFO, "Could not parse SOAP response");
+		ctx->last_err = "Could not parse SOAP response";
+		return NULL;
+	}
+
+	ret = soap_get_body(ctx->xml, resp);
+	if (ret == NULL) {
+		wpa_printf(MSG_INFO, "Could not get SOAP body");
+		ctx->last_err = "Could not get SOAP body";
+		return NULL;
+	}
+
+	wpa_printf(MSG_DEBUG, "SOAP body localname: '%s'",
+		   xml_node_get_localname(ctx->xml, ret));
+	n = xml_node_copy(ctx->xml, ret);
+	xml_node_free(ctx->xml, resp);
+
+	return n;
+}
+
+
+struct http_ctx * http_init_ctx(void *upper_ctx, struct xml_node_ctx *xml_ctx)
+{
+	struct http_ctx *ctx;
+
+	ctx = os_zalloc(sizeof(*ctx));
+	if (ctx == NULL)
+		return NULL;
+	ctx->ctx = upper_ctx;
+	ctx->xml = xml_ctx;
+	ctx->ocsp = OPTIONAL_OCSP;
+
+	curl_global_init(CURL_GLOBAL_ALL);
+
+	return ctx;
+}
+
+
+void http_ocsp_set(struct http_ctx *ctx, int val)
+{
+	if (val == 0)
+		ctx->ocsp = NO_OCSP;
+	else if (val == 1)
+		ctx->ocsp = OPTIONAL_OCSP;
+	if (val == 2)
+		ctx->ocsp = MANDATORY_OCSP;
+}
+
+
+void http_deinit_ctx(struct http_ctx *ctx)
+{
+	clear_curl(ctx);
+	os_free(ctx->curl_buf);
+	curl_global_cleanup();
+
+	os_free(ctx->svc_address);
+	os_free(ctx->svc_ca_fname);
+	os_free(ctx->svc_username);
+	os_free(ctx->svc_password);
+	os_free(ctx->svc_client_cert);
+	os_free(ctx->svc_client_key);
+
+	os_free(ctx);
+}
+
+
+int http_download_file(struct http_ctx *ctx, const char *url,
+		       const char *fname, const char *ca_fname)
+{
+	CURL *curl;
+	FILE *f;
+	CURLcode res;
+	long http = 0;
+
+	ctx->last_err = NULL;
+
+	wpa_printf(MSG_DEBUG, "curl: Download file from %s to %s (ca=%s)",
+		   url, fname, ca_fname);
+	curl = curl_easy_init();
+	if (curl == NULL)
+		return -1;
+
+	f = fopen(fname, "wb");
+	if (f == NULL) {
+		curl_easy_cleanup(curl);
+		return -1;
+	}
+
+	curl_easy_setopt(curl, CURLOPT_URL, url);
+	if (ca_fname) {
+		curl_easy_setopt(curl, CURLOPT_CAINFO, ca_fname);
+		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
+		curl_easy_setopt(curl, CURLOPT_CERTINFO, 1L);
+	} else {
+		curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
+	}
+	curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, curl_cb_debug);
+	curl_easy_setopt(curl, CURLOPT_DEBUGDATA, ctx);
+	curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curl_cb_header);
+	curl_easy_setopt(curl, CURLOPT_HEADERDATA, ctx);
+	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite);
+	curl_easy_setopt(curl, CURLOPT_WRITEDATA, f);
+	curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
+
+	res = curl_easy_perform(curl);
+	if (res != CURLE_OK) {
+		if (!ctx->last_err)
+			ctx->last_err = curl_easy_strerror(res);
+		wpa_printf(MSG_ERROR, "curl_easy_perform() failed: %s",
+			   ctx->last_err);
+		curl_easy_cleanup(curl);
+		fclose(f);
+		return -1;
+	}
+
+	curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http);
+	wpa_printf(MSG_DEBUG, "curl: Server response code %ld", http);
+	if (http != 200) {
+		ctx->last_err = "HTTP download failed";
+		wpa_printf(MSG_INFO, "HTTP download failed - code %ld", http);
+		curl_easy_cleanup(curl);
+		fclose(f);
+		return -1;
+	}
+
+	curl_easy_cleanup(curl);
+	fclose(f);
+
+	return 0;
+}
+
+
+char * http_post(struct http_ctx *ctx, const char *url, const char *data,
+		 const char *content_type, const char *ext_hdr,
+		 const char *ca_fname,
+		 const char *username, const char *password,
+		 const char *client_cert, const char *client_key,
+		 size_t *resp_len)
+{
+	long http = 0;
+	CURLcode res;
+	char *ret;
+	CURL *curl;
+	struct curl_slist *curl_hdr = NULL;
+
+	ctx->last_err = NULL;
+	wpa_printf(MSG_DEBUG, "curl: HTTP POST to %s", url);
+	curl = setup_curl_post(ctx, url, ca_fname, username, password,
+			       client_cert, client_key);
+	if (curl == NULL)
+		return NULL;
+
+	if (content_type) {
+		char ct[200];
+		snprintf(ct, sizeof(ct), "Content-Type: %s", content_type);
+		curl_hdr = curl_slist_append(curl_hdr, ct);
+	}
+	if (ext_hdr)
+		curl_hdr = curl_slist_append(curl_hdr, ext_hdr);
+	curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curl_hdr);
+
+	curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data);
+	free_curl_buf(ctx);
+
+	res = curl_easy_perform(curl);
+	if (res != CURLE_OK) {
+		if (!ctx->last_err)
+			ctx->last_err = curl_easy_strerror(res);
+		wpa_printf(MSG_ERROR, "curl_easy_perform() failed: %s",
+			   ctx->last_err);
+		free_curl_buf(ctx);
+		return NULL;
+	}
+
+	curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http);
+	wpa_printf(MSG_DEBUG, "curl: Server response code %ld", http);
+	if (http != 200) {
+		ctx->last_err = "HTTP POST failed";
+		wpa_printf(MSG_INFO, "HTTP POST failed - code %ld", http);
+		free_curl_buf(ctx);
+		return NULL;
+	}
+
+	if (ctx->curl_buf == NULL)
+		return NULL;
+
+	ret = ctx->curl_buf;
+	if (resp_len)
+		*resp_len = ctx->curl_buf_len;
+	ctx->curl_buf = NULL;
+	ctx->curl_buf_len = 0;
+
+	wpa_printf(MSG_MSGDUMP, "Server response:\n%s", ret);
+
+	return ret;
+}
+
+
+void http_set_cert_cb(struct http_ctx *ctx,
+		      int (*cb)(void *ctx, struct http_cert *cert),
+		      void *cb_ctx)
+{
+	ctx->cert_cb = cb;
+	ctx->cert_cb_ctx = cb_ctx;
+}
+
+
+const char * http_get_err(struct http_ctx *ctx)
+{
+	return ctx->last_err;
+}
diff --git a/src/utils/os.h b/src/utils/os.h
index d63ac29..f019e26 100644
--- a/src/utils/os.h
+++ b/src/utils/os.h
@@ -240,6 +240,13 @@
 char * os_readfile(const char *name, size_t *len);
 
 /**
+ * os_file_exists - Check whether the specified file exists
+ * @fname: Path and name of the file
+ * Returns: 1 if the file exists or 0 if not
+ */
+int os_file_exists(const char *fname);
+
+/**
  * os_zalloc - Allocate and zero memory
  * @size: Number of bytes to allocate
  * Returns: Pointer to allocated and zeroed memory or %NULL on failure
diff --git a/src/utils/os_unix.c b/src/utils/os_unix.c
index fa67fdf..008ec6b 100644
--- a/src/utils/os_unix.c
+++ b/src/utils/os_unix.c
@@ -407,6 +407,16 @@
 }
 
 
+int os_file_exists(const char *fname)
+{
+	FILE *f = fopen(fname, "rb");
+	if (f == NULL)
+		return 0;
+	fclose(f);
+	return 1;
+}
+
+
 #ifndef WPA_TRACE
 void * os_zalloc(size_t size)
 {
diff --git a/src/utils/xml-utils.c b/src/utils/xml-utils.c
new file mode 100644
index 0000000..4916d29
--- /dev/null
+++ b/src/utils/xml-utils.c
@@ -0,0 +1,471 @@
+/*
+ * Generic XML helper functions
+ * Copyright (c) 2012-2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+
+#include "common.h"
+#include "xml-utils.h"
+
+
+static xml_node_t * get_node_uri_iter(struct xml_node_ctx *ctx,
+				      xml_node_t *root, char *uri)
+{
+	char *end;
+	xml_node_t *node;
+	const char *name;
+
+	end = strchr(uri, '/');
+	if (end)
+		*end++ = '\0';
+
+	node = root;
+	xml_node_for_each_sibling(ctx, node) {
+		xml_node_for_each_check(ctx, node);
+		name = xml_node_get_localname(ctx, node);
+		if (strcasecmp(name, uri) == 0)
+			break;
+	}
+
+	if (node == NULL)
+		return NULL;
+
+	if (end) {
+		return get_node_uri_iter(ctx, xml_node_first_child(ctx, node),
+					 end);
+	}
+
+	return node;
+}
+
+
+xml_node_t * get_node_uri(struct xml_node_ctx *ctx, xml_node_t *root,
+			  const char *uri)
+{
+	char *search;
+	xml_node_t *node;
+
+	search = os_strdup(uri);
+	if (search == NULL)
+		return NULL;
+
+	node = get_node_uri_iter(ctx, root, search);
+
+	os_free(search);
+	return node;
+}
+
+
+static xml_node_t * get_node_iter(struct xml_node_ctx *ctx,
+				  xml_node_t *root, const char *path)
+{
+	char *end;
+	xml_node_t *node;
+	const char *name;
+
+	end = os_strchr(path, '/');
+	if (end)
+		*end++ = '\0';
+
+	xml_node_for_each_child(ctx, node, root) {
+		xml_node_for_each_check(ctx, node);
+		name = xml_node_get_localname(ctx, node);
+		if (os_strcasecmp(name, path) == 0)
+			break;
+	}
+
+	if (node == NULL)
+		return NULL;
+	if (end)
+		return get_node_iter(ctx, node, end);
+	return node;
+}
+
+
+xml_node_t * get_node(struct xml_node_ctx *ctx, xml_node_t *root,
+		      const char *path)
+{
+	char *search;
+	xml_node_t *node;
+
+	search = os_strdup(path);
+	if (search == NULL)
+		return NULL;
+
+	node = get_node_iter(ctx, root, search);
+
+	os_free(search);
+	return node;
+}
+
+
+xml_node_t * get_child_node(struct xml_node_ctx *ctx, xml_node_t *root,
+			    const char *path)
+{
+	xml_node_t *node;
+	xml_node_t *match;
+
+	xml_node_for_each_child(ctx, node, root) {
+		xml_node_for_each_check(ctx, node);
+		match = get_node(ctx, node, path);
+		if (match)
+			return match;
+	}
+
+	return NULL;
+}
+
+
+xml_node_t * node_from_file(struct xml_node_ctx *ctx, const char *name)
+{
+	xml_node_t *node;
+	char *buf, *buf2, *start;
+	size_t len;
+
+	buf = os_readfile(name, &len);
+	if (buf == NULL)
+		return NULL;
+	buf2 = os_realloc(buf, len + 1);
+	if (buf2 == NULL) {
+		os_free(buf);
+		return NULL;
+	}
+	buf = buf2;
+	buf[len] = '\0';
+
+	start = os_strstr(buf, "<!DOCTYPE ");
+	if (start) {
+		char *pos = start + 1;
+		int count = 1;
+		while (*pos) {
+			if (*pos == '<')
+				count++;
+			else if (*pos == '>') {
+				count--;
+				if (count == 0) {
+					pos++;
+					break;
+				}
+			}
+			pos++;
+		}
+		if (count == 0) {
+			/* Remove DOCTYPE to allow the file to be parsed */
+			os_memset(start, ' ', pos - start);
+		}
+	}
+
+	node = xml_node_from_buf(ctx, buf);
+	os_free(buf);
+
+	return node;
+}
+
+
+int node_to_file(struct xml_node_ctx *ctx, const char *fname, xml_node_t *node)
+{
+	FILE *f;
+	char *str;
+
+	str = xml_node_to_str(ctx, node);
+	if (str == NULL)
+		return -1;
+
+	f = fopen(fname, "w");
+	if (!f) {
+		os_free(str);
+		return -1;
+	}
+
+	fprintf(f, "%s\n", str);
+	os_free(str);
+	fclose(f);
+
+	return 0;
+}
+
+
+static char * get_val(struct xml_node_ctx *ctx, xml_node_t *node)
+{
+	char *val, *pos;
+
+	val = xml_node_get_text(ctx, node);
+	if (val == NULL)
+		return NULL;
+	pos = val;
+	while (*pos) {
+		if (*pos != ' ' && *pos != '\t' && *pos != '\r' && *pos != '\n')
+			return val;
+		pos++;
+	}
+
+	return NULL;
+}
+
+
+static char * add_path(const char *prev, const char *leaf)
+{
+	size_t len;
+	char *new_uri;
+
+	if (prev == NULL)
+		return NULL;
+
+	len = os_strlen(prev) + 1 + os_strlen(leaf) + 1;
+	new_uri = os_malloc(len);
+	if (new_uri)
+		os_snprintf(new_uri, len, "%s/%s", prev, leaf);
+
+	return new_uri;
+}
+
+
+static void node_to_tnds(struct xml_node_ctx *ctx, xml_node_t *out,
+			 xml_node_t *in, const char *uri)
+{
+	xml_node_t *node;
+	xml_node_t *tnds;
+	const char *name;
+	char *val;
+	char *new_uri;
+
+	xml_node_for_each_child(ctx, node, in) {
+		xml_node_for_each_check(ctx, node);
+		name = xml_node_get_localname(ctx, node);
+
+		tnds = xml_node_create(ctx, out, NULL, "Node");
+		if (tnds == NULL)
+			return;
+		xml_node_create_text(ctx, tnds, NULL, "NodeName", name);
+
+		if (uri)
+			xml_node_create_text(ctx, tnds, NULL, "Path", uri);
+
+		val = get_val(ctx, node);
+		if (val) {
+			xml_node_create_text(ctx, tnds, NULL, "Value", val);
+			xml_node_get_text_free(ctx, val);
+		}
+
+		new_uri = add_path(uri, name);
+		node_to_tnds(ctx, new_uri ? out : tnds, node, new_uri);
+		os_free(new_uri);
+	}
+}
+
+
+static int add_ddfname(struct xml_node_ctx *ctx, xml_node_t *parent,
+		       const char *urn)
+{
+	xml_node_t *node;
+
+	node = xml_node_create(ctx, parent, NULL, "RTProperties");
+	if (node == NULL)
+		return -1;
+	node = xml_node_create(ctx, node, NULL, "Type");
+	if (node == NULL)
+		return -1;
+	xml_node_create_text(ctx, node, NULL, "DDFName", urn);
+	return 0;
+}
+
+
+xml_node_t * mo_to_tnds(struct xml_node_ctx *ctx, xml_node_t *mo,
+			int use_path, const char *urn, const char *ns_uri)
+{
+	xml_node_t *root;
+	xml_node_t *node;
+	const char *name;
+
+	root = xml_node_create_root(ctx, ns_uri, NULL, NULL, "MgmtTree");
+	if (root == NULL)
+		return NULL;
+
+	xml_node_create_text(ctx, root, NULL, "VerDTD", "1.2");
+
+	name = xml_node_get_localname(ctx, mo);
+
+	node = xml_node_create(ctx, root, NULL, "Node");
+	if (node == NULL)
+		goto fail;
+	xml_node_create_text(ctx, node, NULL, "NodeName", name);
+	if (urn)
+		add_ddfname(ctx, node, urn);
+
+	node_to_tnds(ctx, use_path ? root : node, mo, use_path ? name : NULL);
+
+	return root;
+
+fail:
+	xml_node_free(ctx, root);
+	return NULL;
+}
+
+
+static xml_node_t * get_first_child_node(struct xml_node_ctx *ctx,
+					 xml_node_t *node,
+					 const char *name)
+{
+	const char *lname;
+	xml_node_t *child;
+
+	xml_node_for_each_child(ctx, child, node) {
+		xml_node_for_each_check(ctx, child);
+		lname = xml_node_get_localname(ctx, child);
+		if (os_strcasecmp(lname, name) == 0)
+			return child;
+	}
+
+	return NULL;
+}
+
+
+static char * get_node_text(struct xml_node_ctx *ctx, xml_node_t *node,
+			    const char *node_name)
+{
+	node = get_first_child_node(ctx, node, node_name);
+	if (node == NULL)
+		return NULL;
+	return xml_node_get_text(ctx, node);
+}
+
+
+static xml_node_t * add_mo_node(struct xml_node_ctx *ctx, xml_node_t *root,
+				xml_node_t *node, const char *uri)
+{
+	char *nodename, *value, *path;
+	xml_node_t *parent;
+
+	nodename = get_node_text(ctx, node, "NodeName");
+	if (nodename == NULL)
+		return NULL;
+	value = get_node_text(ctx, node, "Value");
+
+	if (root == NULL) {
+		root = xml_node_create_root(ctx, NULL, NULL, NULL,
+					    nodename);
+		if (root && value)
+			xml_node_set_text(ctx, root, value);
+	} else {
+		if (uri == NULL) {
+			xml_node_get_text_free(ctx, nodename);
+			xml_node_get_text_free(ctx, value);
+			return NULL;
+		}
+		path = get_node_text(ctx, node, "Path");
+		if (path)
+			uri = path;
+		parent = get_node_uri(ctx, root, uri);
+		xml_node_get_text_free(ctx, path);
+		if (parent == NULL) {
+			printf("Could not find URI '%s'\n", uri);
+			xml_node_get_text_free(ctx, nodename);
+			xml_node_get_text_free(ctx, value);
+			return NULL;
+		}
+		if (value)
+			xml_node_create_text(ctx, parent, NULL, nodename,
+					     value);
+		else
+			xml_node_create(ctx, parent, NULL, nodename);
+	}
+
+	xml_node_get_text_free(ctx, nodename);
+	xml_node_get_text_free(ctx, value);
+
+	return root;
+}
+
+
+static xml_node_t * tnds_to_mo_iter(struct xml_node_ctx *ctx, xml_node_t *root,
+				    xml_node_t *node, const char *uri)
+{
+	xml_node_t *child;
+	const char *name;
+	char *nodename;
+
+	xml_node_for_each_sibling(ctx, node) {
+		xml_node_for_each_check(ctx, node);
+
+		nodename = get_node_text(ctx, node, "NodeName");
+		if (nodename == NULL)
+			return NULL;
+
+		name = xml_node_get_localname(ctx, node);
+		if (strcmp(name, "Node") == 0) {
+			if (root && !uri) {
+				printf("Invalid TNDS tree structure - "
+				       "multiple top level nodes\n");
+				xml_node_get_text_free(ctx, nodename);
+				return NULL;
+			}
+			root = add_mo_node(ctx, root, node, uri);
+		}
+
+		child = get_first_child_node(ctx, node, "Node");
+		if (child) {
+			if (uri == NULL)
+				tnds_to_mo_iter(ctx, root, child, nodename);
+			else {
+				char *new_uri;
+				new_uri = add_path(uri, nodename);
+				tnds_to_mo_iter(ctx, root, child, new_uri);
+				os_free(new_uri);
+			}
+		}
+		xml_node_get_text_free(ctx, nodename);
+	}
+
+	return root;
+}
+
+
+xml_node_t * tnds_to_mo(struct xml_node_ctx *ctx, xml_node_t *tnds)
+{
+	const char *name;
+	xml_node_t *node;
+
+	name = xml_node_get_localname(ctx, tnds);
+	if (name == NULL || os_strcmp(name, "MgmtTree") != 0)
+		return NULL;
+
+	node = get_first_child_node(ctx, tnds, "Node");
+	if (!node)
+		return NULL;
+	return tnds_to_mo_iter(ctx, NULL, node, NULL);
+}
+
+
+xml_node_t * soap_build_envelope(struct xml_node_ctx *ctx, xml_node_t *node)
+{
+	xml_node_t *envelope, *body;
+	xml_namespace_t *ns;
+
+	envelope = xml_node_create_root(
+		ctx, "http://www.w3.org/2003/05/soap-envelope", "soap12", &ns,
+		"Envelope");
+	if (envelope == NULL)
+		return NULL;
+	body = xml_node_create(ctx, envelope, ns, "Body");
+	xml_node_add_child(ctx, body, node);
+	return envelope;
+}
+
+
+xml_node_t * soap_get_body(struct xml_node_ctx *ctx, xml_node_t *soap)
+{
+	xml_node_t *body, *child;
+
+	body = get_node_uri(ctx, soap, "Envelope/Body");
+	if (body == NULL)
+		return NULL;
+	xml_node_for_each_child(ctx, child, body) {
+		xml_node_for_each_check(ctx, child);
+		return child;
+	}
+	return NULL;
+}
diff --git a/src/utils/xml-utils.h b/src/utils/xml-utils.h
new file mode 100644
index 0000000..0d8e0cb
--- /dev/null
+++ b/src/utils/xml-utils.h
@@ -0,0 +1,100 @@
+/*
+ * Generic XML helper functions
+ * Copyright (c) 2012-2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#ifndef XML_UTILS_H
+#define XML_UTILS_H
+
+struct xml_node_ctx;
+typedef struct xml_node xml_node_t;
+typedef struct xml_namespace_foo xml_namespace_t;
+
+/* XML library wrappers */
+
+int xml_validate(struct xml_node_ctx *ctx, xml_node_t *node,
+		 const char *xml_schema_fname, char **ret_err);
+int xml_validate_dtd(struct xml_node_ctx *ctx, xml_node_t *node,
+		     const char *dtd_fname, char **ret_err);
+void xml_node_free(struct xml_node_ctx *ctx, xml_node_t *node);
+xml_node_t * xml_node_get_parent(struct xml_node_ctx *ctx, xml_node_t *node);
+xml_node_t * xml_node_from_buf(struct xml_node_ctx *ctx, const char *buf);
+const char * xml_node_get_localname(struct xml_node_ctx *ctx,
+				    xml_node_t *node);
+char * xml_node_to_str(struct xml_node_ctx *ctx, xml_node_t *node);
+void xml_node_detach(struct xml_node_ctx *ctx, xml_node_t *node);
+void xml_node_add_child(struct xml_node_ctx *ctx, xml_node_t *parent,
+			xml_node_t *child);
+xml_node_t * xml_node_create_root(struct xml_node_ctx *ctx, const char *ns_uri,
+				  const char *ns_prefix,
+				  xml_namespace_t **ret_ns, const char *name);
+xml_node_t * xml_node_create(struct xml_node_ctx *ctx, xml_node_t *parent,
+			     xml_namespace_t *ns, const char *name);
+xml_node_t * xml_node_create_text(struct xml_node_ctx *ctx,
+				  xml_node_t *parent, xml_namespace_t *ns,
+				  const char *name, const char *value);
+xml_node_t * xml_node_create_text_ns(struct xml_node_ctx *ctx,
+				     xml_node_t *parent, const char *ns_uri,
+				     const char *name, const char *value);
+void xml_node_set_text(struct xml_node_ctx *ctx, xml_node_t *node,
+		       const char *value);
+int xml_node_add_attr(struct xml_node_ctx *ctx, xml_node_t *node,
+		      xml_namespace_t *ns, const char *name, const char *value);
+char * xml_node_get_attr_value(struct xml_node_ctx *ctx, xml_node_t *node,
+			       char *name);
+char * xml_node_get_attr_value_ns(struct xml_node_ctx *ctx, xml_node_t *node,
+				  const char *ns_uri, char *name);
+void xml_node_get_attr_value_free(struct xml_node_ctx *ctx, char *val);
+xml_node_t * xml_node_first_child(struct xml_node_ctx *ctx,
+				  xml_node_t *parent);
+xml_node_t * xml_node_next_sibling(struct xml_node_ctx *ctx,
+				   xml_node_t *node);
+int xml_node_is_element(struct xml_node_ctx *ctx, xml_node_t *node);
+char * xml_node_get_text(struct xml_node_ctx *ctx, xml_node_t *node);
+void xml_node_get_text_free(struct xml_node_ctx *ctx, char *val);
+char * xml_node_get_base64_text(struct xml_node_ctx *ctx, xml_node_t *node,
+				int *ret_len);
+xml_node_t * xml_node_copy(struct xml_node_ctx *ctx, xml_node_t *node);
+
+#define xml_node_for_each_child(ctx, child, parent) \
+for (child = xml_node_first_child(ctx, parent); \
+     child; \
+     child = xml_node_next_sibling(ctx, child))
+
+#define xml_node_for_each_sibling(ctx, node) \
+for (; \
+     node; \
+     node = xml_node_next_sibling(ctx, node))
+
+#define xml_node_for_each_check(ctx, child) \
+if (!xml_node_is_element(ctx, child)) \
+	continue
+
+typedef void (*debug_print_func)(void *ctx, int print, const char *fmt, ...)
+	__attribute__ ((format (printf, 3, 4)));
+
+
+struct xml_node_ctx * xml_node_init_ctx(void *upper_ctx,
+					const void *env);
+void xml_node_deinit_ctx(struct xml_node_ctx *ctx);
+
+
+xml_node_t * get_node_uri(struct xml_node_ctx *ctx, xml_node_t *root,
+			  const char *uri);
+xml_node_t * get_node(struct xml_node_ctx *ctx, xml_node_t *root,
+		      const char *path);
+xml_node_t * get_child_node(struct xml_node_ctx *ctx, xml_node_t *root,
+			    const char *path);
+xml_node_t * node_from_file(struct xml_node_ctx *ctx, const char *name);
+int node_to_file(struct xml_node_ctx *ctx, const char *fname, xml_node_t *node);
+xml_node_t * mo_to_tnds(struct xml_node_ctx *ctx, xml_node_t *mo,
+			int use_path, const char *urn, const char *ns_uri);
+xml_node_t * tnds_to_mo(struct xml_node_ctx *ctx, xml_node_t *tnds);
+
+xml_node_t * soap_build_envelope(struct xml_node_ctx *ctx, xml_node_t *node);
+xml_node_t * soap_get_body(struct xml_node_ctx *ctx, xml_node_t *soap);
+
+#endif /* XML_UTILS_H */
diff --git a/src/utils/xml_libxml2.c b/src/utils/xml_libxml2.c
new file mode 100644
index 0000000..c928394
--- /dev/null
+++ b/src/utils/xml_libxml2.c
@@ -0,0 +1,457 @@
+/*
+ * XML wrapper for libxml2
+ * Copyright (c) 2012-2013, Qualcomm Atheros, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+#define LIBXML_VALID_ENABLED
+#include <libxml/tree.h>
+#include <libxml/xmlschemastypes.h>
+
+#include "common.h"
+#include "base64.h"
+#include "xml-utils.h"
+
+
+struct xml_node_ctx {
+	void *ctx;
+};
+
+
+struct str_buf {
+	char *buf;
+	size_t len;
+};
+
+#define MAX_STR 1000
+
+static void add_str(void *ctx_ptr, const char *fmt, ...)
+{
+	struct str_buf *str = ctx_ptr;
+	va_list ap;
+	char *n;
+	int len;
+
+	n = os_realloc(str->buf, str->len + MAX_STR + 2);
+	if (n == NULL)
+		return;
+	str->buf = n;
+
+	va_start(ap, fmt);
+	len = vsnprintf(str->buf + str->len, MAX_STR, fmt, ap);
+	va_end(ap);
+	if (len >= MAX_STR)
+		len = MAX_STR - 1;
+	str->len += len;
+	str->buf[str->len] = '\0';
+}
+
+
+int xml_validate(struct xml_node_ctx *ctx, xml_node_t *node,
+		 const char *xml_schema_fname, char **ret_err)
+{
+	xmlDocPtr doc;
+	xmlNodePtr n;
+	xmlSchemaParserCtxtPtr pctx;
+	xmlSchemaValidCtxtPtr vctx;
+	xmlSchemaPtr schema;
+	int ret;
+	struct str_buf errors;
+
+	if (ret_err)
+		*ret_err = NULL;
+
+	doc = xmlNewDoc((xmlChar *) "1.0");
+	if (doc == NULL)
+		return -1;
+	n = xmlDocCopyNode((xmlNodePtr) node, doc, 1);
+	if (n == NULL) {
+		xmlFreeDoc(doc);
+		return -1;
+	}
+	xmlDocSetRootElement(doc, n);
+
+	os_memset(&errors, 0, sizeof(errors));
+
+	pctx = xmlSchemaNewParserCtxt(xml_schema_fname);
+	xmlSchemaSetParserErrors(pctx, (xmlSchemaValidityErrorFunc) add_str,
+				 (xmlSchemaValidityWarningFunc) add_str,
+				 &errors);
+	schema = xmlSchemaParse(pctx);
+	xmlSchemaFreeParserCtxt(pctx);
+
+	vctx = xmlSchemaNewValidCtxt(schema);
+	xmlSchemaSetValidErrors(vctx, (xmlSchemaValidityErrorFunc) add_str,
+				(xmlSchemaValidityWarningFunc) add_str,
+				&errors);
+
+	ret = xmlSchemaValidateDoc(vctx, doc);
+	xmlSchemaFreeValidCtxt(vctx);
+	xmlFreeDoc(doc);
+	xmlSchemaFree(schema);
+
+	if (ret == 0) {
+		os_free(errors.buf);
+		return 0;
+	} else if (ret > 0) {
+		if (ret_err)
+			*ret_err = errors.buf;
+		else
+			os_free(errors.buf);
+		return -1;
+	} else {
+		if (ret_err)
+			*ret_err = errors.buf;
+		else
+			os_free(errors.buf);
+		return -1;
+	}
+}
+
+
+int xml_validate_dtd(struct xml_node_ctx *ctx, xml_node_t *node,
+		     const char *dtd_fname, char **ret_err)
+{
+	xmlDocPtr doc;
+	xmlNodePtr n;
+	xmlValidCtxt vctx;
+	xmlDtdPtr dtd;
+	int ret;
+	struct str_buf errors;
+
+	if (ret_err)
+		*ret_err = NULL;
+
+	doc = xmlNewDoc((xmlChar *) "1.0");
+	if (doc == NULL)
+		return -1;
+	n = xmlDocCopyNode((xmlNodePtr) node, doc, 1);
+	if (n == NULL) {
+		xmlFreeDoc(doc);
+		return -1;
+	}
+	xmlDocSetRootElement(doc, n);
+
+	os_memset(&errors, 0, sizeof(errors));
+
+	dtd = xmlParseDTD(NULL, (const xmlChar *) dtd_fname);
+	if (dtd == NULL) {
+		xmlFreeDoc(doc);
+		return -1;
+	}
+
+	os_memset(&vctx, 0, sizeof(vctx));
+	vctx.userData = &errors;
+	vctx.error = add_str;
+	vctx.warning = add_str;
+	ret = xmlValidateDtd(&vctx, doc, dtd);
+	xmlFreeDoc(doc);
+	xmlFreeDtd(dtd);
+
+	if (ret == 1) {
+		os_free(errors.buf);
+		return 0;
+	} else {
+		if (ret_err)
+			*ret_err = errors.buf;
+		else
+			os_free(errors.buf);
+		return -1;
+	}
+}
+
+
+void xml_node_free(struct xml_node_ctx *ctx, xml_node_t *node)
+{
+	xmlFreeNode((xmlNodePtr) node);
+}
+
+
+xml_node_t * xml_node_get_parent(struct xml_node_ctx *ctx, xml_node_t *node)
+{
+	return (xml_node_t *) ((xmlNodePtr) node)->parent;
+}
+
+
+xml_node_t * xml_node_from_buf(struct xml_node_ctx *ctx, const char *buf)
+{
+	xmlDocPtr doc;
+	xmlNodePtr node;
+
+	doc = xmlParseMemory(buf, strlen(buf));
+	if (doc == NULL)
+		return NULL;
+	node = xmlDocGetRootElement(doc);
+	node = xmlCopyNode(node, 1);
+	xmlFreeDoc(doc);
+
+	return (xml_node_t *) node;
+}
+
+
+const char * xml_node_get_localname(struct xml_node_ctx *ctx,
+				    xml_node_t *node)
+{
+	return (const char *) ((xmlNodePtr) node)->name;
+}
+
+
+char * xml_node_to_str(struct xml_node_ctx *ctx, xml_node_t *node)
+{
+	xmlChar *buf;
+	int bufsiz;
+	char *ret, *pos;
+	xmlNodePtr n = (xmlNodePtr) node;
+	xmlDocPtr doc;
+
+	doc = xmlNewDoc((xmlChar *) "1.0");
+	n = xmlDocCopyNode(n, doc, 1);
+	xmlDocSetRootElement(doc, n);
+	xmlDocDumpFormatMemory(doc, &buf, &bufsiz, 0);
+	xmlFreeDoc(doc);
+	pos = (char *) buf;
+	if (strncmp(pos, "<?xml", 5) == 0) {
+		pos = strchr(pos, '>');
+		if (pos)
+			pos++;
+		while (pos && (*pos == '\r' || *pos == '\n'))
+			pos++;
+	}
+	if (pos)
+		ret = os_strdup(pos);
+	else
+		ret = NULL;
+	xmlFree(buf);
+
+	if (ret) {
+		pos = ret;
+		if (pos[0]) {
+			while (pos[1])
+				pos++;
+		}
+		while (pos >= ret && *pos == '\n')
+			*pos-- = '\0';
+	}
+
+	return ret;
+}
+
+
+void xml_node_detach(struct xml_node_ctx *ctx, xml_node_t *node)
+{
+	xmlUnlinkNode((xmlNodePtr) node);
+}
+
+
+void xml_node_add_child(struct xml_node_ctx *ctx, xml_node_t *parent,
+			xml_node_t *child)
+{
+	xmlAddChild((xmlNodePtr) parent, (xmlNodePtr) child);
+}
+
+
+xml_node_t * xml_node_create_root(struct xml_node_ctx *ctx, const char *ns_uri,
+				  const char *ns_prefix,
+				  xml_namespace_t **ret_ns, const char *name)
+{
+	xmlNodePtr node;
+	xmlNsPtr ns = NULL;
+
+	node = xmlNewNode(NULL, (const xmlChar *) name);
+	if (node == NULL)
+		return NULL;
+	if (ns_uri) {
+		ns = xmlNewNs(node, (const xmlChar *) ns_uri,
+			      (const xmlChar *) ns_prefix);
+		xmlSetNs(node, ns);
+	}
+
+	if (ret_ns)
+		*ret_ns = (xml_namespace_t *) ns;
+
+	return (xml_node_t *) node;
+}
+
+
+xml_node_t * xml_node_create(struct xml_node_ctx *ctx, xml_node_t *parent,
+			     xml_namespace_t *ns, const char *name)
+{
+	xmlNodePtr node;
+	node = xmlNewChild((xmlNodePtr) parent, (xmlNsPtr) ns,
+			   (const xmlChar *) name, NULL);
+	return (xml_node_t *) node;
+}
+
+
+xml_node_t * xml_node_create_text(struct xml_node_ctx *ctx,
+				  xml_node_t *parent, xml_namespace_t *ns,
+				  const char *name, const char *value)
+{
+	xmlNodePtr node;
+	node = xmlNewTextChild((xmlNodePtr) parent, (xmlNsPtr) ns,
+			       (const xmlChar *) name, (const xmlChar *) value);
+	return (xml_node_t *) node;
+}
+
+
+xml_node_t * xml_node_create_text_ns(struct xml_node_ctx *ctx,
+				     xml_node_t *parent, const char *ns_uri,
+				     const char *name, const char *value)
+{
+	xmlNodePtr node;
+	xmlNsPtr ns;
+
+	node = xmlNewTextChild((xmlNodePtr) parent, NULL,
+			       (const xmlChar *) name, (const xmlChar *) value);
+	ns = xmlNewNs(node, (const xmlChar *) ns_uri, NULL);
+	xmlSetNs(node, ns);
+	return (xml_node_t *) node;
+}
+
+
+void xml_node_set_text(struct xml_node_ctx *ctx, xml_node_t *node,
+		       const char *value)
+{
+	/* TODO: escape XML special chars in value */
+	xmlNodeSetContent((xmlNodePtr) node, (xmlChar *) value);
+}
+
+
+int xml_node_add_attr(struct xml_node_ctx *ctx, xml_node_t *node,
+		      xml_namespace_t *ns, const char *name, const char *value)
+{
+	xmlAttrPtr attr;
+
+	if (ns) {
+		attr = xmlNewNsProp((xmlNodePtr) node, (xmlNsPtr) ns,
+				    (const xmlChar *) name,
+				    (const xmlChar *) value);
+	} else {
+		attr = xmlNewProp((xmlNodePtr) node, (const xmlChar *) name,
+				  (const xmlChar *) value);
+	}
+
+	return attr ? 0 : -1;
+}
+
+
+char * xml_node_get_attr_value(struct xml_node_ctx *ctx, xml_node_t *node,
+			       char *name)
+{
+	return (char *) xmlGetNoNsProp((xmlNodePtr) node,
+				       (const xmlChar *) name);
+}
+
+
+char * xml_node_get_attr_value_ns(struct xml_node_ctx *ctx, xml_node_t *node,
+				  const char *ns_uri, char *name)
+{
+	return (char *) xmlGetNsProp((xmlNodePtr) node, (const xmlChar *) name,
+				     (const xmlChar *) ns_uri);
+}
+
+
+void xml_node_get_attr_value_free(struct xml_node_ctx *ctx, char *val)
+{
+	if (val)
+		xmlFree((xmlChar *) val);
+}
+
+
+xml_node_t * xml_node_first_child(struct xml_node_ctx *ctx,
+				  xml_node_t *parent)
+{
+	return (xml_node_t *) ((xmlNodePtr) parent)->children;
+}
+
+
+xml_node_t * xml_node_next_sibling(struct xml_node_ctx *ctx,
+				   xml_node_t *node)
+{
+	return (xml_node_t *) ((xmlNodePtr) node)->next;
+}
+
+
+int xml_node_is_element(struct xml_node_ctx *ctx, xml_node_t *node)
+{
+	return ((xmlNodePtr) node)->type == XML_ELEMENT_NODE;
+}
+
+
+char * xml_node_get_text(struct xml_node_ctx *ctx, xml_node_t *node)
+{
+	if (xmlChildElementCount((xmlNodePtr) node) > 0)
+		return NULL;
+	return (char *) xmlNodeGetContent((xmlNodePtr) node);
+}
+
+
+void xml_node_get_text_free(struct xml_node_ctx *ctx, char *val)
+{
+	if (val)
+		xmlFree((xmlChar *) val);
+}
+
+
+char * xml_node_get_base64_text(struct xml_node_ctx *ctx, xml_node_t *node,
+				int *ret_len)
+{
+	char *txt;
+	unsigned char *ret;
+	size_t len;
+
+	txt = xml_node_get_text(ctx, node);
+	if (txt == NULL)
+		return NULL;
+
+	ret = base64_decode((unsigned char *) txt, strlen(txt), &len);
+	if (ret_len)
+		*ret_len = len;
+	xml_node_get_text_free(ctx, txt);
+	if (ret == NULL)
+		return NULL;
+	txt = os_malloc(len + 1);
+	if (txt == NULL) {
+		os_free(ret);
+		return NULL;
+	}
+	os_memcpy(txt, ret, len);
+	txt[len] = '\0';
+	return txt;
+}
+
+
+xml_node_t * xml_node_copy(struct xml_node_ctx *ctx, xml_node_t *node)
+{
+	if (node == NULL)
+		return NULL;
+	return (xml_node_t *) xmlCopyNode((xmlNodePtr) node, 1);
+}
+
+
+struct xml_node_ctx * xml_node_init_ctx(void *upper_ctx,
+					const void *env)
+{
+	struct xml_node_ctx *xctx;
+
+	xctx = os_zalloc(sizeof(*xctx));
+	if (xctx == NULL)
+		return NULL;
+	xctx->ctx = upper_ctx;
+
+	LIBXML_TEST_VERSION
+
+	return xctx;
+}
+
+
+void xml_node_deinit_ctx(struct xml_node_ctx *ctx)
+{
+	xmlSchemaCleanupTypes();
+	xmlCleanupParser();
+	xmlMemoryDump();
+	os_free(ctx);
+}