User override of server certificate

Store server certificate hash and TOD policy from WPA3-Enterprise
connection attempts with sta_associate. Add support for dev_exec_action
ServerCertTrust,Accept argument. If TOD policy is in place for the
current network profile, reject that command; if TOD policy is not in
place, update network profile to use the last received server
certificate (from the last failed connection attempt) and try to
reconnect.

Signed-off-by: Jouni Malinen <jouni@codeaurora.org>
diff --git a/dev.c b/dev.c
index ada3f90..e70b7fa 100644
--- a/dev.c
+++ b/dev.c
@@ -67,11 +67,129 @@
 }
 
 
+static enum sigma_cmd_result sta_server_cert_trust(struct sigma_dut *dut,
+						   struct sigma_conn *conn,
+						   const char *val)
+{
+	char buf[100];
+	struct wpa_ctrl *ctrl = NULL;
+	int e;
+	char resp[200];
+	int num_disconnected = 0;
+
+	strlcpy(resp, "ServerCertTrustResult,Accepted", sizeof(resp));
+
+	if (strcasecmp(val, "Accept") != 0) {
+		strlcpy(resp,
+			"ServerCertTrustResult,NotAccepted,Reason,Unsupported ServerCertTrust value",
+			sizeof(resp));
+		goto done;
+	}
+
+	if (!dut->server_cert_hash[0]) {
+		strlcpy(resp,
+			"ServerCertTrustResult,NotAccepted,Reason,No server certificate stored",
+			sizeof(resp));
+		goto done;
+	}
+
+	if (dut->sta_tod_policy) {
+		strlcpy(resp,
+			"ServerCertTrustResult,NotAccepted,Reason,TOD policy",
+			sizeof(resp));
+		goto done;
+	}
+
+	snprintf(buf, sizeof(buf), "hash://server/sha256/%s",
+		 dut->server_cert_hash);
+	if (set_network_quoted(get_station_ifname(), dut->infra_network_id,
+			       "ca_cert", buf) < 0) {
+		strlcpy(resp,
+			"ServerCertTrustResult,NotAccepted,Reason,Could not configure server certificate hash for the network profile",
+			sizeof(resp));
+		goto done;
+	}
+
+	wpa_command(get_station_ifname(), "DISCONNECT");
+	snprintf(buf, sizeof(buf), "SELECT_NETWORK %d", dut->infra_network_id);
+	if (wpa_command(get_station_ifname(), buf) < 0) {
+		sigma_dut_print(dut, DUT_MSG_INFO, "Failed to select "
+				"network id %d on %s",
+				dut->infra_network_id,
+				get_station_ifname());
+		strlcpy(resp,
+			"ServerCertTrustResult,Accepted,Result,Could not request reconnection",
+			sizeof(resp));
+		goto done;
+	}
+
+	ctrl = open_wpa_mon(get_station_ifname());
+	if (!ctrl)
+		goto done;
+
+	for (e = 0; e < 20; e++) {
+		const char *events[] = {
+			"CTRL-EVENT-EAP-TLS-CERT-ERROR",
+			"CTRL-EVENT-DISCONNECTED",
+			"CTRL-EVENT-CONNECTED",
+			NULL
+		};
+		char buf[1024];
+		int res;
+
+		res = get_wpa_cli_events(dut, ctrl, events, buf, sizeof(buf));
+		if (res < 0) {
+			strlcpy(resp,
+				"ServerCertTrustResult,Accepted,Result,Association did not complete",
+				sizeof(resp));
+			goto done;
+		}
+		sigma_dut_print(dut, DUT_MSG_DEBUG, "Connection event: %s",
+				buf);
+
+		if (strstr(buf, "CTRL-EVENT-EAP-TLS-CERT-ERROR")) {
+			strlcpy(resp,
+				"ServerCertTrustResult,Accepted,Result,TLS server certitficate validation failed with updated profile",
+				sizeof(resp));
+			goto done;
+		}
+
+		if (strstr(buf, "CTRL-EVENT-DISCONNECTED")) {
+			num_disconnected++;
+
+			if (num_disconnected > 2) {
+				strlcpy(resp,
+					"ServerCertTrustResult,Accepted,Result,Connection failed",
+					sizeof(resp));
+				goto done;
+			}
+		}
+
+		if (strstr(buf, "CTRL-EVENT-CONNECTED")) {
+				strlcpy(resp,
+					"ServerCertTrustResult,Accepted,Result,Connected",
+					sizeof(resp));
+			break;
+		}
+	}
+
+done:
+	if (ctrl) {
+		wpa_ctrl_detach(ctrl);
+		wpa_ctrl_close(ctrl);
+	}
+
+	send_resp(dut, conn, SIGMA_COMPLETE, resp);
+	return STATUS_SENT;
+}
+
+
 static enum sigma_cmd_result cmd_dev_exec_action(struct sigma_dut *dut,
 						 struct sigma_conn *conn,
 						 struct sigma_cmd *cmd)
 {
 	const char *program = get_param(cmd, "Program");
+	const char *val;
 
 #ifdef MIRACAST
 	if (program && (strcasecmp(program, "WFD") == 0 ||
@@ -85,6 +203,10 @@
 	if (program && strcasecmp(program, "DPP") == 0)
 		return dpp_dev_exec_action(dut, conn, cmd);
 
+	val = get_param(cmd, "ServerCertTrust");
+	if (val)
+		return sta_server_cert_trust(dut, conn, val);
+
 	return ERROR_SEND_STATUS;
 }
 
diff --git a/sigma_dut.h b/sigma_dut.h
index 0a172cb..7ef07b9 100644
--- a/sigma_dut.h
+++ b/sigma_dut.h
@@ -834,6 +834,9 @@
 
 	char *sae_commit_override;
 	char *rsne_override;
+	int sta_associate_wait_connect;
+	char server_cert_hash[65];
+	int sta_tod_policy;
 	const char *hostapd_bin;
 	int use_hostapd_pid_file;
 	const char *hostapd_ifname;
diff --git a/sta.c b/sta.c
index 39371db..04dc9cb 100644
--- a/sta.c
+++ b/sta.c
@@ -2334,6 +2334,8 @@
 	if (erp && set_network(ifname, id, "erp", "1") < 0)
 		return ERROR_SEND_STATUS;
 
+	dut->sta_associate_wait_connect = 1;
+
 	return id;
 }
 
@@ -3221,6 +3223,12 @@
 	const char *network_mode = get_param(cmd, "network_mode");
 	int wps = 0;
 	char buf[1000], extra[50];
+	int e;
+	enum sigma_cmd_result ret = SUCCESS_SEND_STATUS;
+	struct wpa_ctrl *ctrl = NULL;
+	int num_network_not_found = 0;
+	int num_disconnected = 0;
+	int tod = -1;
 
 	if (ssid == NULL)
 		return -1;
@@ -3300,6 +3308,13 @@
 			return 0;
 		}
 
+		if (dut->program == PROGRAM_WPA3 &&
+		    dut->sta_associate_wait_connect) {
+			ctrl = open_wpa_mon(get_station_ifname());
+			if (!ctrl)
+				return ERROR_SEND_STATUS;
+		}
+
 		extra[0] = '\0';
 		if (chan)
 			snprintf(extra, sizeof(extra), " freq=%u",
@@ -3311,11 +3326,109 @@
 					"network id %d on %s",
 					dut->infra_network_id,
 					get_station_ifname());
-			return -2;
+			ret = ERROR_SEND_STATUS;
+			goto done;
 		}
 	}
 
-	return 1;
+	if (!ctrl)
+		return SUCCESS_SEND_STATUS;
+
+	/* Wait for connection result to be able to store server certificate
+	 * hash for trust root override testing
+	 * (dev_exec_action,ServerCertTrust). */
+
+	for (e = 0; e < 20; e++) {
+		const char *events[] = {
+			"CTRL-EVENT-EAP-PEER-CERT",
+			"CTRL-EVENT-EAP-TLS-CERT-ERROR",
+			"CTRL-EVENT-DISCONNECTED",
+			"CTRL-EVENT-CONNECTED",
+			"CTRL-EVENT-NETWORK-NOT-FOUND",
+			NULL
+		};
+		char buf[1024];
+		int res;
+
+		res = get_wpa_cli_events(dut, ctrl, events, buf, sizeof(buf));
+		if (res < 0) {
+			send_resp(dut, conn, SIGMA_ERROR,
+				  "ErrorCode,Association did not complete");
+			ret = STATUS_SENT_ERROR;
+			break;
+		}
+		sigma_dut_print(dut, DUT_MSG_DEBUG, "Connection event: %s",
+				buf);
+
+		if (strstr(buf, "CTRL-EVENT-EAP-PEER-CERT") &&
+		    strstr(buf, " depth=0")) {
+			char *pos = strstr(buf, " hash=");
+
+			if (pos) {
+				char *end;
+
+				tod = strstr(buf, " tod=1") != NULL;
+				sigma_dut_print(dut, DUT_MSG_DEBUG,
+						"Server certificate TOD policy: %d",
+						tod);
+
+				pos += 6;
+				end = strchr(pos, ' ');
+				if (end)
+					*end = '\0';
+				strlcpy(dut->server_cert_hash, pos,
+					sizeof(dut->server_cert_hash));
+				sigma_dut_print(dut, DUT_MSG_DEBUG,
+						"Server certificate hash: %s",
+						dut->server_cert_hash);
+			}
+		}
+
+		if (strstr(buf, "CTRL-EVENT-EAP-TLS-CERT-ERROR")) {
+			send_resp(dut, conn, SIGMA_COMPLETE,
+				  "Result,TLS server certificate validation failed");
+			ret = STATUS_SENT_ERROR;
+			break;
+		}
+
+		if (strstr(buf, "CTRL-EVENT-NETWORK-NOT-FOUND")) {
+			num_network_not_found++;
+
+			if (num_network_not_found > 2) {
+				send_resp(dut, conn, SIGMA_COMPLETE,
+					  "Result,Network not found");
+				ret = STATUS_SENT_ERROR;
+				break;
+			}
+		}
+
+		if (strstr(buf, "CTRL-EVENT-DISCONNECTED")) {
+			num_disconnected++;
+
+			if (num_disconnected > 2) {
+				send_resp(dut, conn, SIGMA_COMPLETE,
+					  "Result,Connection failed");
+				ret = STATUS_SENT_ERROR;
+				break;
+			}
+		}
+
+		if (strstr(buf, "CTRL-EVENT-CONNECTED")) {
+			if (tod >= 0) {
+				sigma_dut_print(dut, DUT_MSG_DEBUG,
+						"Network profile TOD policy update: %d -> %d",
+						dut->sta_tod_policy, tod);
+				dut->sta_tod_policy = tod;
+			}
+			break;
+		}
+	}
+done:
+	if (ctrl) {
+		wpa_ctrl_detach(ctrl);
+		wpa_ctrl_close(ctrl);
+	}
+	return ret;
 }
 
 
@@ -7516,6 +7629,10 @@
 	free(dut->sae_commit_override);
 	dut->sae_commit_override = NULL;
 
+	dut->sta_associate_wait_connect = 0;
+	dut->server_cert_hash[0] = '\0';
+	dut->sta_tod_policy = 0;
+
 	dut->dpp_conf_id = -1;
 	free(dut->dpp_peer_uri);
 	dut->dpp_peer_uri = NULL;