diff --git a/src/dpp-common.c b/src/dpp-common.c
index 45099d12..2d4bbb31 100644
--- a/src/dpp-common.c
+++ b/src/dpp-common.c
@@ -145,6 +145,70 @@ struct dpp_sm {
 	bool initiator : 1;
 };
 
+/*
+ * Builds only the core DPP header. This is shared between DPP over 80211 and
+ * over TCP. Those protocols are required to encapsulate the core message as
+ * required by the spec.
+ */
+static size_t dpp_build_header(enum dpp_frame_type type, uint8_t *buf)
+{
+	uint8_t *ptr = buf;
+
+	*ptr++ = DPP_ACTION_VENDOR_SPECIFIC;
+	memcpy(ptr, wifi_alliance_oui, 3);
+	ptr += 3;
+	*ptr++ = 0x1a;			/* WiFi Alliance DPP OI type */
+	*ptr++ = 1;			/* Cryptosuite */
+	*ptr++ = type;
+
+	return ptr - buf;
+}
+
+/*
+ * For some reason the DPP spec decided to use GAS frame formats only for the
+ * configuration request and response. There is no explicit DPP frame type for
+ * these messages, hence the boolean 'request' parameter. The header will be
+ * formatted either for a configuration request or configuration response.
+ *
+ * See Section 8.3 DPP Generic Advertisement Service (GAS) frames
+ *
+ * As with the more generic header above, the specific protocol handlers will
+ * be required to add extra header information (80211 or TCP).
+ */
+static size_t dpp_build_config_header(bool request, uint8_t diag_token,
+					uint8_t *buf)
+{
+	uint8_t *ptr = buf;
+
+	*ptr++ = request ? DPP_ACTION_GAS_REQUEST: DPP_ACTION_GAS_RESPONSE;
+	*ptr++ = diag_token;
+
+	if (!request) {
+		/* Status */
+		l_put_le16(0, ptr);
+		ptr += 2;
+		/* Not fragmented */
+		l_put_le16(0, ptr);
+		ptr += 2;
+	}
+
+	*ptr++ = IE_TYPE_ADVERTISEMENT_PROTOCOL;
+	*ptr++ = 8; /* len */
+	if (request)
+		*ptr++ = 0x7f;
+	else
+		*ptr++ = 0x00;
+
+	*ptr++ = IE_TYPE_VENDOR_SPECIFIC;
+	*ptr++ = 5;
+	memcpy(ptr, wifi_alliance_oui, 3);
+	ptr += 3;
+	*ptr++ = 0x1a;
+	*ptr++ = 1;
+
+	return ptr - buf;
+}
+
 static void dpp_failed(struct dpp_sm *dpp)
 {
 	if (dpp->event_cb)
@@ -169,6 +233,503 @@ static void dpp_reset_protocol_timer(struct dpp_sm *dpp, uint64_t time)
 						dpp, NULL);
 }
 
+static bool dpp_check_roles(struct dpp_sm *dpp, uint8_t peer_capa)
+{
+	if (dpp->role == DPP_CAPABILITY_ENROLLEE &&
+			!(peer_capa & DPP_CAPABILITY_CONFIGURATOR))
+		return false;
+	else if (dpp->role == DPP_CAPABILITY_CONFIGURATOR &&
+			!(peer_capa & DPP_CAPABILITY_ENROLLEE))
+		return false;
+
+	return true;
+}
+
+static void dpp_try_write(struct dpp_sm *dpp, const uint8_t *frame,
+				size_t frame_len)
+{
+	if (!dpp->write) {
+		if (dpp->frame_pending) {
+			l_free(dpp->frame_pending);
+			dpp->frame_pending = NULL;
+		}
+
+		l_debug("No write handler, waiting to send");
+		dpp->frame_pending = l_memdup(frame, frame_len);
+		dpp->frame_len = frame_len;
+		return;
+	}
+
+	dpp->write(frame, frame_len, dpp->user_data);
+}
+
+static void dpp_send_presence(struct dpp_sm *dpp)
+{
+	uint8_t frame[DPP_HDR_LEN + 64];
+	uint8_t *ptr = frame;
+
+	ptr += dpp_build_header(DPP_FRAME_PRESENCE_ANNOUNCEMENT, ptr);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_RESPONDER_BOOT_KEY_HASH,
+				dpp->own_chirp_hash,
+				sizeof(dpp->own_chirp_hash));
+
+	dpp->state = DPP_STATE_PRESENCE;
+
+	l_debug("sending presence announcement");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_auth_request(struct dpp_sm *dpp)
+{
+	uint8_t frame[DPP_HDR_LEN + 256];
+	uint8_t *ptr = frame;
+	uint8_t version = 2;
+
+	ptr += dpp_build_header(DPP_FRAME_AUTHENTICATION_REQUEST, ptr);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_RESPONDER_BOOT_KEY_HASH,
+				dpp->peer_boot_hash, 32);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_INITIATOR_BOOT_KEY_HASH,
+				dpp->own_boot_hash, 32);
+	ptr += dpp_append_point(ptr, DPP_ATTR_INITIATOR_PROTOCOL_KEY,
+				dpp->own_proto_public);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_PROTOCOL_VERSION, &version, 1);
+
+	if (dpp->role == DPP_CAPABILITY_CONFIGURATOR && dpp->channel[0])
+		ptr += dpp_append_attr(ptr, DPP_ATTR_CHANNEL, dpp->channel, 2);
+
+	ptr += dpp_append_wrapped_data(frame, sizeof(frame), ptr,
+			dpp->k1, dpp->key_len, 2,
+			DPP_ATTR_INITIATOR_NONCE, dpp->nonce_len, dpp->i_nonce,
+			DPP_ATTR_INITIATOR_CAPABILITIES,
+			(size_t) 1, &dpp->role);
+
+	dpp->state = DPP_STATE_AUTHENTICATING;
+
+	l_debug("sending auth request");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_auth_response(struct dpp_sm *dpp)
+{
+	uint8_t frame[DPP_HDR_LEN + 512];
+	uint8_t *ptr = frame;
+	uint8_t status = DPP_STATUS_OK;
+	uint8_t version = 2;
+	uint8_t wrapped2_plaintext[dpp->key_len + 4];
+	uint8_t wrapped2[dpp->key_len + 16 + 8];
+	size_t wrapped2_len;
+
+	ptr += dpp_build_header(DPP_FRAME_AUTHENTICATION_RESPONSE, ptr);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_STATUS, &status, 1);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_RESPONDER_BOOT_KEY_HASH,
+				dpp->own_boot_hash, 32);
+	if (dpp->mutual_auth)
+		ptr += dpp_append_attr(ptr, DPP_ATTR_INITIATOR_BOOT_KEY_HASH,
+				dpp->peer_boot_hash, 32);
+	ptr += dpp_append_point(ptr, DPP_ATTR_RESPONDER_PROTOCOL_KEY,
+				dpp->own_proto_public);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_PROTOCOL_VERSION, &version, 1);
+
+	/* Wrap up secondary data (R-Auth) */
+	wrapped2_len = dpp_append_attr(wrapped2_plaintext,
+					DPP_ATTR_RESPONDER_AUTH_TAG,
+					dpp->auth_tag, dpp->key_len);
+	/*
+	 * "Invocations of AES-SIV in the DPP Authentication protocol that
+	 * produce ciphertext that is part of an additional AES-SIV invocation
+	 * do not use AAD; in other words, the number of AAD components is set
+	 * to zero."
+	 */
+	if (!aes_siv_encrypt(dpp->ke, dpp->key_len, wrapped2_plaintext,
+					dpp->key_len + 4, NULL, 0, wrapped2)) {
+		l_error("Failed to encrypt wrapped data");
+		return;
+	}
+
+	wrapped2_len += 16;
+
+	ptr += dpp_append_wrapped_data(frame, sizeof(frame), ptr,
+			dpp->k2, dpp->key_len, 4,
+			DPP_ATTR_RESPONDER_NONCE, dpp->nonce_len, dpp->r_nonce,
+			DPP_ATTR_INITIATOR_NONCE, dpp->nonce_len, dpp->i_nonce,
+			DPP_ATTR_RESPONDER_CAPABILITIES, (size_t) 1, &dpp->role,
+			DPP_ATTR_WRAPPED_DATA, wrapped2_len, wrapped2);
+
+	l_debug("send auth response");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_auth_request_failed(struct dpp_sm *dpp,
+					enum dpp_status status,
+					void *k1)
+{
+	uint8_t frame[DPP_HDR_LEN + 128];
+	uint8_t *ptr = frame;
+	uint8_t version = 2;
+	uint8_t s = status;
+
+	ptr += dpp_build_header(DPP_FRAME_AUTHENTICATION_RESPONSE, ptr);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_STATUS, &s, 1);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_RESPONDER_BOOT_KEY_HASH,
+				dpp->own_boot_hash, 32);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_PROTOCOL_VERSION, &version, 1);
+	ptr += dpp_append_wrapped_data(frame, sizeof(frame), ptr,
+			k1, dpp->key_len, 2,
+			DPP_ATTR_INITIATOR_NONCE, dpp->nonce_len, dpp->i_nonce,
+			DPP_ATTR_RESPONDER_CAPABILITIES,
+			(size_t) 1, &dpp->role);
+
+	l_debug("sending auth request failed");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_auth_confirm(struct dpp_sm *dpp)
+{
+	uint8_t frame[DPP_HDR_LEN + 256];
+	uint8_t *ptr = frame;
+	uint8_t zero = 0;
+
+	ptr += dpp_build_header(DPP_FRAME_AUTHENTICATION_CONFIRM, ptr);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_STATUS, &zero, 1);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_RESPONDER_BOOT_KEY_HASH,
+					dpp->peer_boot_hash, 32);
+	if (dpp->mutual_auth)
+		ptr += dpp_append_attr(ptr, DPP_ATTR_INITIATOR_BOOT_KEY_HASH,
+					dpp->own_boot_hash, 32);
+
+	ptr += dpp_append_wrapped_data(frame, sizeof(frame), ptr,
+			dpp->ke, dpp->key_len, 1,
+			DPP_ATTR_INITIATOR_AUTH_TAG, dpp->key_len,
+			dpp->auth_tag);
+
+	l_debug("sending auth confirm");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_config_request(struct dpp_sm *dpp)
+{
+	const char *json = "{\"name\":\"IWD\",\"wi-fi_tech\":\"infra\","
+				"\"netRole\":\"sta\"}";
+	size_t json_len = strlen(json);
+	uint8_t frame[DPP_HDR_LEN + 512];
+	uint8_t *ptr = frame;
+	uint8_t *lptr;
+
+	l_getrandom(&dpp->diag_token, 1);
+	l_getrandom(dpp->e_nonce, dpp->nonce_len);
+
+	ptr += dpp_build_config_header(true, dpp->diag_token, ptr);
+
+	/* Save length location */
+	lptr = ptr;
+	ptr += 2;
+
+	ptr += dpp_append_wrapped_data(frame, sizeof(frame), ptr,
+			dpp->ke, dpp->key_len, 2,
+			DPP_ATTR_ENROLLEE_NONCE, dpp->nonce_len, dpp->e_nonce,
+			DPP_ATTR_CONFIGURATION_REQUEST, json_len, json);
+
+	l_put_le16(ptr - lptr - 2, lptr);
+
+	dpp->state = DPP_STATE_CONFIGURING;
+
+	l_debug("sending config request");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_config_response(struct dpp_sm *dpp, uint8_t status)
+{
+	_auto_(l_free) char *json = NULL;
+	uint8_t frame[512];
+	size_t json_len;
+	uint8_t *ptr = frame;
+	uint8_t *lptr;
+
+	ptr += dpp_build_config_header(false, dpp->diag_token, ptr);
+
+	/* Save length location */
+	lptr = ptr;
+	ptr += 2;
+
+	ptr += dpp_append_attr(ptr, DPP_ATTR_STATUS, &status, 1);
+
+	/*
+	 * There are several failure status codes that can be used (defined in
+	 * 6.4.3.1), each with their own set of attributes that should be
+	 * included. For now IWD's basic DPP implementation will assume
+	 * STATUS_CONFIGURE_FAILURE which only includes the E-Nonce.
+	 */
+	if (status == DPP_STATUS_OK) {
+		json = dpp_configuration_to_json(dpp->config);
+		json_len = strlen(json);
+
+		ptr += dpp_append_wrapped_data(frame, sizeof(frame), ptr,
+						dpp->ke, dpp->key_len, 2,
+						DPP_ATTR_ENROLLEE_NONCE,
+						dpp->nonce_len, dpp->e_nonce,
+						DPP_ATTR_CONFIGURATION_OBJECT,
+						json_len, json);
+	} else
+		ptr += dpp_append_wrapped_data(frame, sizeof(frame), ptr,
+						dpp->ke, dpp->key_len, 2,
+						DPP_ATTR_ENROLLEE_NONCE,
+						dpp->nonce_len, dpp->e_nonce);
+
+	l_put_le16(ptr - lptr - 2, lptr);
+
+	l_debug("sending config response");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_config_result(struct dpp_sm *dpp)
+{
+	uint8_t frame[DPP_HDR_LEN + 256];
+	uint8_t *ptr = frame;
+	uint8_t zero = 0;
+
+	memset(frame, 0, sizeof(frame));
+
+	ptr += dpp_build_header(DPP_FRAME_CONFIGURATION_RESULT, ptr);
+	ptr += dpp_append_wrapped_data(frame, sizeof(frame), ptr,
+			dpp->ke, dpp->key_len, 2,
+			DPP_ATTR_STATUS, (size_t) 1, &zero,
+			DPP_ATTR_ENROLLEE_NONCE, dpp->nonce_len, dpp->e_nonce);
+
+	l_debug("sending config result");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_pkex_exchange_request(struct dpp_sm *dpp)
+{
+	uint8_t frame[DPP_HDR_LEN + 256];
+	uint8_t *ptr = frame;
+	uint16_t group;
+
+	memset(frame, 0, sizeof(frame));
+
+	l_put_le16(l_ecc_curve_get_ike_group(dpp->curve), &group);
+
+	ptr += dpp_build_header(DPP_FRAME_PKEX_VERSION1_XCHG_REQUEST, ptr);
+
+	ptr += dpp_append_attr(ptr, DPP_ATTR_PROTOCOL_VERSION,
+				&dpp->pkex_version, 1);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_FINITE_CYCLIC_GROUP,
+				&group, 2);
+
+	if (dpp->pkex_id)
+		ptr += dpp_append_attr(ptr, DPP_ATTR_CODE_IDENTIFIER,
+					dpp->pkex_id, strlen(dpp->pkex_id));
+
+	ptr += dpp_append_point(ptr, DPP_ATTR_ENCRYPTED_KEY, dpp->pkex_m);
+
+	dpp->state = DPP_STATE_PKEX_EXCHANGE;
+
+	l_debug("sending PKEX v1 exchange request");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_pkex_exchange_response(struct dpp_sm *dpp,
+						struct l_ecc_point *n)
+{
+	uint8_t frame[DPP_HDR_LEN + 256];
+	uint8_t *ptr = frame;
+	uint16_t group;
+	uint8_t status = DPP_STATUS_OK;
+
+	memset(frame, 0, sizeof(frame));
+
+	l_put_le16(l_ecc_curve_get_ike_group(dpp->curve), &group);
+
+	ptr += dpp_build_header(DPP_FRAME_PKEX_XCHG_RESPONSE, ptr);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_STATUS, &status, 1);
+
+	if (dpp->pkex_id)
+		ptr += dpp_append_attr(ptr, DPP_ATTR_CODE_IDENTIFIER,
+					dpp->pkex_id, strlen(dpp->pkex_id));
+
+	ptr += dpp_append_point(ptr, DPP_ATTR_ENCRYPTED_KEY, n);
+
+	dpp->state = DPP_STATE_PKEX_COMMIT_REVEAL;
+
+	l_debug("sending PKEX exchange response");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_pkex_bad_group(struct dpp_sm *dpp)
+{
+	uint8_t frame[DPP_HDR_LEN + 256];
+	uint8_t *ptr = frame;
+	uint16_t group;
+	uint8_t status = DPP_STATUS_BAD_GROUP;
+
+	memset(frame, 0, sizeof(frame));
+
+	l_put_le16(l_ecc_curve_get_ike_group(dpp->curve), &group);
+
+	ptr += dpp_build_header(DPP_FRAME_PKEX_XCHG_RESPONSE, ptr);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_STATUS, &status, 1);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_PROTOCOL_VERSION,
+				&dpp->pkex_version, 1);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_FINITE_CYCLIC_GROUP, &group, 2);
+
+	l_debug("sending PKEX bad group");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_pkex_bad_code(struct dpp_sm *dpp)
+{
+	uint8_t frame[DPP_HDR_LEN + 256];
+	uint8_t *ptr = frame;
+	uint8_t status = DPP_STATUS_BAD_CODE;
+
+	memset(frame, 0, sizeof(frame));
+
+	ptr += dpp_build_header(DPP_FRAME_PKEX_XCHG_RESPONSE, ptr);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_STATUS, &status, 1);
+	ptr += dpp_append_attr(ptr, DPP_ATTR_PROTOCOL_VERSION,
+				&dpp->pkex_version, 1);
+	if (dpp->pkex_id)
+		ptr += dpp_append_attr(ptr, DPP_ATTR_CODE_IDENTIFIER,
+					dpp->pkex_id, strlen(dpp->pkex_id));
+
+	l_debug("sending PKEX bad code");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_pkex_commit_reveal_request(struct dpp_sm *dpp)
+{
+	uint8_t frame[DPP_HDR_LEN + 512];
+	uint8_t *ptr = frame;
+	uint8_t a_pub[L_ECC_POINT_MAX_BYTES];
+	ssize_t a_len;
+
+	memset(frame, 0, sizeof(frame));
+
+	a_len = l_ecc_point_get_data(dpp->boot_public, a_pub, sizeof(a_pub));
+
+	ptr += dpp_build_header(DPP_FRAME_PKEX_COMMIT_REVEAL_REQUEST, ptr);
+
+	ptr += dpp_append_wrapped_data(frame, sizeof(frame), ptr,
+			dpp->z, dpp->z_len, 2,
+			DPP_ATTR_BOOTSTRAPPING_KEY, a_len, a_pub,
+			DPP_ATTR_INITIATOR_AUTH_TAG, dpp->u_len, dpp->u);
+
+	dpp->state = DPP_STATE_PKEX_COMMIT_REVEAL;
+
+	l_debug("sending PKEX commit reveal request");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_send_pkex_commit_reveal_response(struct dpp_sm *dpp,
+						const uint8_t *v, size_t v_len)
+{
+	uint8_t frame[DPP_HDR_LEN + 512];
+	uint8_t *ptr = frame;
+	uint8_t b_pub[L_ECC_POINT_MAX_BYTES];
+	size_t b_len;
+
+	memset(frame, 0, sizeof(frame));
+
+	b_len = l_ecc_point_get_data(dpp->boot_public, b_pub, sizeof(b_pub));
+
+	ptr += dpp_build_header(DPP_FRAME_PKEX_COMMIT_REVEAL_RESPONSE, ptr);
+	ptr += dpp_append_wrapped_data(frame, sizeof(frame), ptr,
+			dpp->z, dpp->z_len, 2,
+			DPP_ATTR_BOOTSTRAPPING_KEY, b_len, b_pub,
+			DPP_ATTR_RESPONDER_AUTH_TAG, v_len, v);
+
+	l_debug("sending PKEX commit reveal response");
+
+	dpp_try_write(dpp, frame, ptr - frame);
+}
+
+static void dpp_process_pkex_exchange_request(struct dpp_sm *dpp,
+						struct l_ecc_point *m)
+{
+	_auto_(l_ecc_point_free) struct l_ecc_point *n = NULL;
+	_auto_(l_ecc_point_free) struct l_ecc_point *qr = NULL;
+	_auto_(l_ecc_point_free) struct l_ecc_point *qi = NULL;
+	_auto_(l_ecc_point_free) struct l_ecc_point *k = NULL;
+
+	l_debug("");
+
+	/* Qi = H(MAC-Initiator | [identifier | ] code) * Pi */
+	qi = dpp_derive_qi(dpp->curve, dpp->pkex_key, dpp->pkex_id,
+				dpp->mac_initiator);
+	if (!qi) {
+		l_debug("could not derive Qi");
+		return;
+	}
+
+	/* X' = M - Qi */
+	dpp->y_or_x = l_ecc_point_new(dpp->curve);
+
+	l_ecc_point_inverse(qi);
+	l_ecc_point_add(dpp->y_or_x, m, qi);
+
+	/*
+	 * "The resulting ephemeral key, denoted X’, is checked whether it is
+	 * the point-at-infinity. If it is not valid, the protocol silently
+	 * fails"
+	 */
+	if (l_ecc_point_is_infinity(dpp->y_or_x)) {
+		l_debug("X' is at infinity, ignore message");
+		dpp_failed(dpp);
+		return;
+	}
+
+	qr = dpp_derive_qr(dpp->curve, dpp->pkex_key, dpp->pkex_id,
+				dpp->mac_responder);
+	if (!qr || l_ecc_point_is_infinity(qr)) {
+		l_debug("Qr did not derive");
+		l_ecc_point_free(dpp->y_or_x);
+		dpp->y_or_x = NULL;
+		goto bad_code;
+	}
+
+	/*
+	 * "The Responder then generates a random ephemeral keypair, y/Y,
+	 * encrypts Y with Qr to obtain the result, denoted N."
+	 */
+	l_ecdh_generate_key_pair(dpp->curve, &dpp->pkex_private,
+					&dpp->pkex_public);
+
+	/* N = Y + Qr */
+	n = l_ecc_point_new(dpp->curve);
+
+	l_ecc_point_add(n, dpp->pkex_public, qr);
+
+	/* K = y * X' */
+
+	k = l_ecc_point_new(dpp->curve);
+
+	l_ecc_point_multiply(k, dpp->pkex_private, dpp->y_or_x);
+
+	/* z = HKDF(<>, info | M.x | N.x | code, K.x) */
+	dpp_derive_z(dpp->mac_initiator, dpp->mac_responder, n, m, k,
+			dpp->pkex_key, dpp->pkex_id, dpp->z, &dpp->z_len);
+
+	dpp_send_pkex_exchange_response(dpp, n);
+
+	return;
+
+bad_code:
+	dpp_send_pkex_bad_code(dpp);
+}
+
 struct dpp_sm *dpp_sm_new(dpp_event_cb_t event,
 				const struct l_ecc_point *boot_public,
 				const struct l_ecc_scalar *boot_private,
@@ -201,7 +762,25 @@ void dpp_sm_set_write_handler(struct dpp_sm *dpp, dpp_write_cb_t write)
 	if (!dpp->write)
 		return;
 
-	/* Handle writing frame */
+	/*
+	 * Presence/exchange are special cases because 802.11 encapsulation
+	 * requires going off channel for specific amounts of time. We cannot
+	 * easily start a timer within the common code and try and remained
+	 * synced with the offchannel callbacks.
+	 * Instead let the offchannel callbacks dictate when these frames
+	 * are sent out. All other write operations are driven by the reception
+	 * of other frames.
+	 */
+	if (dpp->state == DPP_STATE_PRESENCE && !dpp->initiator)
+		dpp_send_presence(dpp);
+	else if ( dpp->state == DPP_STATE_PKEX_EXCHANGE && dpp->initiator)
+		dpp_send_pkex_exchange_request(dpp);
+	else if (dpp->frame_pending) {
+		dpp->write(dpp->frame_pending, dpp->frame_len, dpp->user_data);
+
+		l_free(dpp->frame_pending);
+		dpp->frame_pending = NULL;
+	}
 }
 
 static void dpp_free_auth_data(struct dpp_sm *dpp)
@@ -339,6 +918,9 @@ static bool dpp_start_authentication(struct dpp_sm *dpp)
 
 	dpp->state = DPP_STATE_AUTHENTICATING;
 
+	if (dpp->initiator)
+		dpp_send_auth_request(dpp);
+
 	return true;
 }
 
@@ -413,6 +995,16 @@ bool dpp_sm_start_responder(struct dpp_sm *dpp)
 					&dpp->own_proto_public);
 	dpp->initiator = false;
 
+	/*
+	 * An Enrollee acting in a Responder role uses DPP Presence
+	 * Announcement to signal a potential Configurator that it is
+	 * ready to engage in a DPP exchange
+	 */
+	if (dpp->role == DPP_CAPABILITY_ENROLLEE) {
+		if (!dpp->skip_presence)
+			dpp_send_presence(dpp);
+	}
+
 	dpp->state = DPP_STATE_PRESENCE;
 
 	return true;
@@ -426,6 +1018,10 @@ void dpp_sm_set_pkex_identifier(struct dpp_sm *dpp, const char *identifier)
 void dpp_sm_set_pkex_key(struct dpp_sm *dpp, const char *key)
 {
 	dpp->pkex_key = l_strdup(key);
+
+	/* This was set after the key was requested, send the response now */
+	if (dpp->state == DPP_STATE_PKEX_EXCHANGE)
+		dpp_process_pkex_exchange_request(dpp, dpp->peer_encr_key);
 }
 
 void dpp_sm_set_pkex_own_mac(struct dpp_sm *dpp, const uint8_t *mac)
@@ -488,7 +1084,7 @@ bool dpp_sm_pkex_start_initiator(struct dpp_sm *dpp)
 
 	dpp_reset_protocol_timer(dpp, DPP_AUTH_PROTO_TIMEOUT);
 
-	/* Send exchange request */
+	dpp_send_pkex_exchange_request(dpp);
 
 	return true;
 
@@ -512,7 +1108,1649 @@ bool dpp_sm_pkex_start_responder(struct dpp_sm *dpp)
 	return true;
 }
 
-void dpp_handle_rx(struct dpp_sm *dpp, const uint8_t *data, size_t len)
+static void dpp_handle_presence_announcement(struct dpp_sm *dpp,
+						const uint8_t *frame,
+						size_t frame_len)
+{
+	struct dpp_attr_iter iter;
+	enum dpp_attribute_type type;
+	size_t len;
+	const uint8_t *data;
+	const void *r_boot = NULL;
+	size_t r_boot_len = 0;
+	uint8_t hash[32];
+
+	l_debug("Presence announcement");
+
+	/* Must be a configurator, in an initiator role, in PRESENCE state */
+	if (dpp->state != DPP_STATE_PRESENCE)
+		return;
+
+	if (dpp->role != DPP_CAPABILITY_CONFIGURATOR)
+		return;
+
+	dpp_attr_iter_init(&iter, frame + 6, frame_len - 6);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_RESPONDER_BOOT_KEY_HASH:
+			r_boot = data;
+			r_boot_len = len;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!r_boot || r_boot_len != 32) {
+		l_debug("No responder boot hash");
+		return;
+	}
+
+	/* Hash what we have for the peer and check its our enrollee */
+	dpp_hash(L_CHECKSUM_SHA256, hash, 2, "chirp", strlen("chirp"),
+			dpp->peer_asn1, dpp->peer_asn1_len);
+
+	if (memcmp(hash, r_boot, sizeof(hash))) {
+		l_debug("Peers boot hash did not match");
+		return;
+	}
+
+	if (dpp->event_cb)
+		dpp->event_cb(DPP_EVENT_PEER_ACCEPTED, NULL, dpp->user_data);
+
+	dpp->state = DPP_STATE_AUTHENTICATING;
+
+	dpp_reset_protocol_timer(dpp, DPP_AUTH_PROTO_TIMEOUT);
+
+	dpp_start_authentication(dpp);
+}
+
+static void dpp_handle_auth_request(struct dpp_sm *dpp,
+					const uint8_t *frame, size_t frame_len)
 {
-	/* Handle frame */
+	struct dpp_attr_iter iter;
+	enum dpp_attribute_type type;
+	size_t len;
+	const uint8_t *data;
+	const uint8_t *r_boot = NULL;
+	const uint8_t *i_boot = NULL;
+	const uint8_t *i_proto = NULL;
+	const void *wrapped = NULL;
+	const uint8_t *i_nonce = NULL;
+	uint8_t i_capa = 0;
+	size_t r_boot_len = 0, i_proto_len = 0, wrapped_len = 0;
+	size_t i_nonce_len = 0;
+	_auto_(l_free) uint8_t *unwrapped = NULL;
+	_auto_(l_ecc_scalar_free) struct l_ecc_scalar *m = NULL;
+	_auto_(l_ecc_scalar_free) struct l_ecc_scalar *n = NULL;
+	_auto_(l_ecc_point_free) struct l_ecc_point *l = NULL;
+	struct l_ecc_point *bi = NULL;
+	uint64_t k1[L_ECC_MAX_DIGITS];
+	const void *ad0 = frame;
+	const void *ad1 = frame + 6;
+
+	if (dpp->state != DPP_STATE_PRESENCE &&
+				dpp->state != DPP_STATE_AUTHENTICATING)
+		return;
+
+	l_debug("authenticate request");
+
+	dpp_attr_iter_init(&iter, frame + 6, frame_len - 6);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_INITIATOR_BOOT_KEY_HASH:
+			i_boot = data;
+			/*
+			 * This attribute is required by the spec, but only
+			 * used for mutual authentication.
+			 */
+			break;
+		case DPP_ATTR_RESPONDER_BOOT_KEY_HASH:
+			r_boot = data;
+			r_boot_len = len;
+			break;
+		case DPP_ATTR_INITIATOR_PROTOCOL_KEY:
+			i_proto = data;
+			i_proto_len = len;
+			break;
+		case DPP_ATTR_WRAPPED_DATA:
+			/* I-Nonce/I-Capabilities part of wrapped data */
+			wrapped = data;
+			wrapped_len = len;
+			break;
+
+		/* Optional attributes */
+		case DPP_ATTR_PROTOCOL_VERSION:
+			if (l_get_u8(data) != 2) {
+				l_debug("Protocol version did not match");
+				return;
+			}
+
+			break;
+
+		case DPP_ATTR_CHANNEL:
+			if (len != 2)
+				return;
+
+			/*
+			 * Not part of the spec, but IWD puts a requirement on
+			 * enrollees that they must come to the configurators
+			 * channel to preserve performance
+			 */
+			if (dpp->role == DPP_CAPABILITY_CONFIGURATOR)
+				break;
+
+			if (dpp->event_cb)
+				dpp->event_cb(DPP_EVENT_CHANNEL_SWITCH, data,
+						dpp->user_data);
+
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!r_boot || !i_boot || !i_proto || !wrapped) {
+		l_debug("missing attributes");
+		goto auth_request_failed;
+	}
+
+	if (r_boot_len != 32 || memcmp(dpp->own_boot_hash,
+					r_boot, r_boot_len)) {
+		l_debug("Responder boot key hash failed to verify");
+		goto auth_request_failed;
+	}
+
+	dpp->peer_proto_public = l_ecc_point_from_data(dpp->curve,
+						L_ECC_POINT_TYPE_FULL,
+						i_proto, i_proto_len);
+	if (!dpp->peer_proto_public) {
+		l_debug("Initiators protocol key invalid");
+		goto auth_request_failed;
+	}
+
+	m = dpp_derive_k1(dpp->peer_proto_public, dpp->boot_private, k1);
+	if (!m) {
+		l_debug("deriving k1/m failed");
+		goto auth_request_failed;
+	}
+
+	unwrapped = dpp_unwrap_attr(ad0, 6, ad1, wrapped - 4 - ad1,
+			k1, dpp->key_len, wrapped, wrapped_len, &wrapped_len);
+	if (!unwrapped) {
+		l_debug("failed to unwrap data");
+		goto auth_request_failed;
+	}
+
+	dpp_attr_iter_init(&iter, unwrapped, wrapped_len);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_INITIATOR_NONCE:
+			i_nonce = data;
+			i_nonce_len = len;
+			break;
+		case DPP_ATTR_INITIATOR_CAPABILITIES:
+			/*
+			 * "If the Responder is not capable of supporting the
+			 * role indicated by the Initiator, it shall respond
+			 * with a DPP Authentication Response frame indicating
+			 * failure by adding the DPP Status field set to
+			 * STATUS_NOT_COMPATIBLE"
+			 */
+			i_capa = l_get_u8(data);
+
+			if (!dpp_check_roles(dpp, i_capa)) {
+				l_debug("Peer does not support required role");
+				dpp_send_auth_request_failed(dpp,
+						DPP_STATUS_NOT_COMPATIBLE, k1);
+				goto auth_request_failed;
+			}
+
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (i_nonce_len != dpp->nonce_len) {
+		l_debug("I-Nonce has unexpected length %zu", i_nonce_len);
+		goto auth_request_failed;
+	}
+
+	memcpy(dpp->i_nonce, i_nonce, i_nonce_len);
+
+	if (dpp->mutual_auth) {
+		l = dpp_derive_lr(dpp->boot_private, dpp->proto_private,
+					dpp->peer_boot_public);
+		bi = dpp->peer_boot_public;
+	}
+
+	/* Derive keys k2, ke, and R-Auth for authentication response */
+
+	n = dpp_derive_k2(dpp->peer_proto_public, dpp->proto_private, dpp->k2);
+	if (!n) {
+		l_debug("deriving k2/n failed");
+		goto auth_request_failed;
+	}
+
+	l_getrandom(dpp->r_nonce, dpp->nonce_len);
+
+	if (!dpp_derive_ke(dpp->i_nonce, dpp->r_nonce, m, n, l, dpp->ke)) {
+		l_debug("deriving ke failed");
+		goto auth_request_failed;
+	}
+
+	if (!dpp_derive_r_auth(dpp->i_nonce, dpp->r_nonce, dpp->nonce_len,
+				dpp->peer_proto_public, dpp->own_proto_public,
+				bi, dpp->boot_public, dpp->auth_tag)) {
+		l_debug("deriving R_auth failed");
+		goto auth_request_failed;
+	}
+
+	dpp->state = DPP_STATE_AUTHENTICATING;
+
+	dpp_reset_protocol_timer(dpp, DPP_AUTH_PROTO_TIMEOUT);
+
+	dpp_send_auth_response(dpp);
+
+	return;
+
+auth_request_failed:
+	l_debug("handle auth request failed");
+	dpp->state = DPP_STATE_PRESENCE;
+	dpp_free_auth_data(dpp);
+}
+
+static void dpp_handle_auth_response(struct dpp_sm *dpp,
+					const uint8_t *frame, size_t frame_len)
+{
+	struct dpp_attr_iter iter;
+	enum dpp_attribute_type type;
+	size_t len;
+	const uint8_t *data;
+	int status = -1;
+	const void *r_boot_hash = NULL;
+	const void *r_proto = NULL;
+	size_t r_proto_len = 0;
+	const void *wrapped = NULL;
+	size_t wrapped_len;
+	_auto_(l_free) uint8_t *unwrapped1 = NULL;
+	_auto_(l_free) uint8_t *unwrapped2 = NULL;
+	const void *r_nonce = NULL;
+	const void *i_nonce = NULL;
+	const void *r_auth = NULL;
+	_auto_(l_ecc_point_free) struct l_ecc_point *r_proto_key = NULL;
+	_auto_(l_ecc_scalar_free) struct l_ecc_scalar *n = NULL;
+	_auto_(l_ecc_point_free) struct l_ecc_point *l = NULL;
+	struct l_ecc_point *bi = NULL;
+	const void *ad0 = frame;
+	const void *ad1 = frame + 6;
+	uint64_t r_auth_derived[L_ECC_MAX_DIGITS];
+
+	l_debug("Authenticate response");
+
+	if (dpp->state != DPP_STATE_AUTHENTICATING)
+		return;
+
+	dpp_attr_iter_init(&iter, frame + 6, frame_len - 6);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_STATUS:
+			if (len != 1)
+				return;
+
+			status = l_get_u8(data);
+			break;
+		case DPP_ATTR_RESPONDER_BOOT_KEY_HASH:
+			r_boot_hash = data;
+			break;
+		case DPP_ATTR_RESPONDER_PROTOCOL_KEY:
+			r_proto = data;
+			r_proto_len = len;
+			break;
+		case DPP_ATTR_PROTOCOL_VERSION:
+			if (len != 1 || l_get_u8(data) != 2)
+				return;
+			break;
+		case DPP_ATTR_WRAPPED_DATA:
+			wrapped = data;
+			wrapped_len = len;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (status != DPP_STATUS_OK || !r_boot_hash || !r_proto || !wrapped) {
+		l_debug("Auth response bad status or missing attributes");
+		return;
+	}
+
+	r_proto_key = l_ecc_point_from_data(dpp->curve, L_ECC_POINT_TYPE_FULL,
+						r_proto, r_proto_len);
+	if (!r_proto_key) {
+		l_debug("Peers protocol key was invalid");
+		return;
+	}
+
+	n = dpp_derive_k2(r_proto_key, dpp->proto_private, dpp->k2);
+
+	unwrapped1 = dpp_unwrap_attr(ad0, 6, ad1, wrapped - 4 - ad1, dpp->k2,
+					dpp->key_len, wrapped, wrapped_len,
+					&wrapped_len);
+	if (!unwrapped1) {
+		l_debug("Failed to unwrap primary data");
+		return;
+	}
+
+	wrapped = NULL;
+
+	dpp_attr_iter_init(&iter, unwrapped1, wrapped_len);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_RESPONDER_NONCE:
+			if (len != dpp->nonce_len)
+				return;
+
+			r_nonce = data;
+			break;
+		case DPP_ATTR_INITIATOR_NONCE:
+			if (len != dpp->nonce_len)
+				return;
+
+			i_nonce = data;
+			break;
+		case DPP_ATTR_RESPONDER_CAPABILITIES:
+			break;
+		case DPP_ATTR_WRAPPED_DATA:
+			wrapped = data;
+			wrapped_len = len;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!r_nonce || !i_nonce || !wrapped) {
+		l_debug("Wrapped data missing attributes");
+		return;
+	}
+
+	if (dpp->mutual_auth) {
+		l = dpp_derive_li(dpp->peer_boot_public, r_proto_key,
+					dpp->boot_private);
+		bi = dpp->boot_public;
+	}
+
+	if (!dpp_derive_ke(i_nonce, r_nonce, dpp->m, n, l, dpp->ke)) {
+		l_debug("Failed to derive ke");
+		return;
+	}
+
+	unwrapped2 = dpp_unwrap_attr(NULL, 0, NULL, 0, dpp->ke, dpp->key_len,
+					wrapped, wrapped_len, &wrapped_len);
+	if (!unwrapped2) {
+		l_debug("Failed to unwrap secondary data");
+		return;
+	}
+
+	dpp_attr_iter_init(&iter, unwrapped2, wrapped_len);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_RESPONDER_AUTH_TAG:
+			if (len != dpp->key_len)
+				return;
+
+			r_auth = data;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!r_auth) {
+		l_debug("R-Auth was not in secondary wrapped data");
+		return;
+	}
+
+	if (!dpp_derive_r_auth(i_nonce, r_nonce, dpp->nonce_len,
+				dpp->own_proto_public, r_proto_key, bi,
+				dpp->peer_boot_public, r_auth_derived)) {
+		l_debug("Failed to derive r_auth");
+		return;
+	}
+
+	if (memcmp(r_auth, r_auth_derived, dpp->key_len)) {
+		l_debug("R-Auth did not verify");
+		return;
+	}
+
+	if (!dpp_derive_i_auth(r_nonce, i_nonce, dpp->nonce_len,
+				r_proto_key, dpp->own_proto_public,
+				dpp->peer_boot_public, bi, dpp->auth_tag)) {
+		l_debug("Could not derive I-Auth");
+		return;
+	}
+
+	dpp_send_auth_confirm(dpp);
+
+	if (dpp->role == DPP_CAPABILITY_ENROLLEE)
+		dpp_send_config_request(dpp);
+}
+
+static void dpp_handle_auth_confirm(struct dpp_sm *dpp,
+					const uint8_t *frame, size_t frame_len)
+{
+	struct dpp_attr_iter iter;
+	enum dpp_attribute_type type;
+	size_t len;
+	const uint8_t *data;
+	int status = -1;
+	const uint8_t *r_boot_hash = NULL;
+	const void *wrapped = NULL;
+	const uint8_t *i_auth = NULL;
+	size_t i_auth_len;
+	_auto_(l_free) uint8_t *unwrapped = NULL;
+	size_t wrapped_len = 0;
+	uint64_t i_auth_check[L_ECC_MAX_DIGITS];
+	const void *unwrap_key;
+	const void *ad0 = frame;
+	const void *ad1 = frame + 6;
+	struct l_ecc_point *bi = NULL;
+
+	if (dpp->state != DPP_STATE_AUTHENTICATING)
+		return;
+
+	l_debug("authenticate confirm");
+
+	dpp_attr_iter_init(&iter, frame + 6, frame_len - 6);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_STATUS:
+			status = l_get_u8(data);
+			break;
+		case DPP_ATTR_RESPONDER_BOOT_KEY_HASH:
+			r_boot_hash = data;
+			/*
+			 * Spec requires this, but does not mention if anything
+			 * is to be done with it.
+			 */
+			break;
+		case DPP_ATTR_INITIATOR_BOOT_KEY_HASH:
+			/* No mutual authentication */
+			break;
+		case DPP_ATTR_WRAPPED_DATA:
+			wrapped = data;
+			wrapped_len = len;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!r_boot_hash || !wrapped) {
+		l_debug("Attributes missing from authenticate confirm");
+		return;
+	}
+
+	/*
+	 * "The Responder obtains the DPP Authentication Confirm frame and
+	 * checks the value of the DPP Status field. If the value of the DPP
+	 * Status field is STATUS_NOT_COMPATIBLE or STATUS_AUTH_FAILURE, the
+	 * Responder unwraps the wrapped data portion of the frame using k2"
+	 */
+	if (status == DPP_STATUS_OK)
+		unwrap_key = dpp->ke;
+	else if (status == DPP_STATUS_NOT_COMPATIBLE ||
+				status == DPP_STATUS_AUTH_FAILURE)
+		unwrap_key = dpp->k2;
+	else
+		goto auth_confirm_failed;
+
+	unwrapped = dpp_unwrap_attr(ad0, 6, ad1, wrapped - 4 - ad1,
+			unwrap_key, dpp->key_len, wrapped, wrapped_len,
+			&wrapped_len);
+	if (!unwrapped)
+		goto auth_confirm_failed;
+
+	if (status != DPP_STATUS_OK) {
+		/*
+		 * "If unwrapping is successful, the Responder should generate
+		 * an alert indicating the reason for the protocol failure."
+		 */
+		l_debug("Authentication failed due to status %s",
+				status == DPP_STATUS_NOT_COMPATIBLE ?
+				"NOT_COMPATIBLE" : "AUTH_FAILURE");
+		goto auth_confirm_failed;
+	}
+
+	dpp_attr_iter_init(&iter, unwrapped, wrapped_len);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_INITIATOR_AUTH_TAG:
+			i_auth = data;
+			i_auth_len = len;
+			break;
+		case DPP_ATTR_RESPONDER_NONCE:
+			/* Only if error */
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!i_auth || i_auth_len != dpp->key_len) {
+		l_debug("I-Auth missing from wrapped data");
+		goto auth_confirm_failed;
+	}
+
+	if (dpp->mutual_auth)
+		bi = dpp->peer_boot_public;
+
+	dpp_derive_i_auth(dpp->r_nonce, dpp->i_nonce, dpp->nonce_len,
+				dpp->own_proto_public, dpp->peer_proto_public,
+				dpp->boot_public, bi, i_auth_check);
+
+	if (memcmp(i_auth, i_auth_check, i_auth_len)) {
+		l_error("I-Auth did not verify");
+		goto auth_confirm_failed;
+	}
+
+	l_debug("Authentication successful");
+
+	if (dpp->role == DPP_CAPABILITY_ENROLLEE)
+		dpp_send_config_request(dpp);
+
+	return;
+
+auth_confirm_failed:
+	dpp->state = DPP_STATE_PRESENCE;
+	dpp_free_auth_data(dpp);
+}
+
+/* Parses the config header (GAS) and returns the start of the payload */
+static const uint8_t *dpp_parse_config_header(uint8_t action,
+						const uint8_t *data, size_t len,
+						uint8_t *diag_token_out,
+						uint16_t *status_out,
+						uint16_t *comeback_out,
+						size_t *attr_len_out)
+{
+	uint8_t adv_protocol_id[] = { 0xDD, 0x05, 0x50, 0x6F,
+					0x9A, 0x1A, 0x01 };
+	uint8_t diag_token;
+	uint16_t status = 0;
+	uint16_t comeback = 0;
+	uint16_t attr_len;
+	const uint8_t *ptr = data;
+
+	if (len < 1)
+		return NULL;
+
+	diag_token = *ptr++;
+
+	switch (action) {
+	case DPP_ACTION_GAS_REQUEST:
+		if (len < 12)
+			return NULL;
+		break;
+	case DPP_ACTION_GAS_RESPONSE:
+		if (len < 14)
+			return NULL;
+
+		status = l_get_le16(ptr);
+		ptr += 2;
+		comeback = l_get_le16(ptr);
+		ptr += 2;
+
+		break;
+	default:
+		return NULL;
+	}
+
+	if (*ptr++ != IE_TYPE_ADVERTISEMENT_PROTOCOL)
+		return NULL;
+
+	/* Length of advertisement protocol fields */
+	if (*ptr++ != 0x08)
+		return NULL;
+	/*
+	 * Unfortunately wpa_supplicant hard codes 0x7f as the Query Response
+	 * Info so we need to handle both cases.
+	 */
+	if (*ptr != 0x7f && *ptr != 0x00)
+		return NULL;
+	ptr++;
+
+	if (memcmp(ptr, adv_protocol_id, sizeof(adv_protocol_id)))
+		return NULL;
+
+	ptr += sizeof(adv_protocol_id);
+
+	attr_len = l_get_le16(ptr);
+	ptr += 2;
+
+	/* Check the attribute length matches the total length */
+	if (attr_len > len - (ptr - data))
+		return false;
+
+	if (diag_token_out)
+		*diag_token_out = diag_token;
+
+	if (action == DPP_ACTION_GAS_RESPONSE) {
+		if (status_out)
+			*status_out = status;
+		if (comeback_out)
+			*comeback_out = comeback;
+	}
+
+	if (attr_len_out)
+		*attr_len_out = attr_len;
+
+	return ptr;
+}
+
+static void dpp_handle_config_request(struct dpp_sm *dpp, const uint8_t *frame,
+				size_t frame_len)
+{
+	const uint8_t *attrs;
+	size_t attrs_len;
+	struct dpp_attr_iter iter;
+	enum dpp_attribute_type type;
+	const uint8_t *data;
+	size_t len;
+	const char *json = NULL;
+	size_t json_len = 0;
+	struct json_contents *c;
+	const uint8_t *wrapped = NULL;
+	const uint8_t *e_nonce = NULL;
+	size_t wrapped_len = 0;
+	_auto_(l_free) uint8_t *unwrapped = NULL;
+	struct json_iter jsiter;
+	_auto_(l_free) char *tech = NULL;
+	_auto_(l_free) char *role = NULL;
+
+	attrs = dpp_parse_config_header(DPP_ACTION_GAS_REQUEST, frame,
+					frame_len, &dpp->diag_token,
+					NULL, NULL, &attrs_len);
+	if (!attrs)
+		return;
+
+	if (dpp->state != DPP_STATE_AUTHENTICATING) {
+		l_debug("Configuration request in wrong state");
+		return;
+	}
+
+	if (dpp->role != DPP_CAPABILITY_CONFIGURATOR)
+		return;
+
+	dpp_attr_iter_init(&iter, attrs, attrs_len);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_WRAPPED_DATA:
+			wrapped = data;
+			wrapped_len = len;
+			break;
+		default:
+			/* Wrapped data should be only attribute */
+			return;
+		}
+	}
+
+	if (!wrapped) {
+		l_debug("Wrapped data missing");
+		return;
+	}
+
+	unwrapped = dpp_unwrap_attr(NULL, 0, NULL, 0, dpp->ke,
+					dpp->key_len, wrapped, wrapped_len,
+					&wrapped_len);
+	if (!unwrapped) {
+		l_debug("Failed to unwrap");
+		return;
+	}
+
+	dpp_attr_iter_init(&iter, unwrapped, wrapped_len);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_ENROLLEE_NONCE:
+			if (len != dpp->nonce_len)
+				break;
+
+			e_nonce = data;
+			break;
+		case DPP_ATTR_CONFIGURATION_REQUEST:
+			json = (const char *)data;
+			json_len = len;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!json || !e_nonce) {
+		l_debug("No configuration object in response");
+		return;
+	}
+
+	c = json_contents_new(json, json_len);
+	if (!c) {
+		json_contents_free(c);
+		return;
+	}
+
+	json_iter_init(&jsiter, c);
+
+	/*
+	 * Check mandatory values (Table 7). There isn't much that can be done
+	 * with these, but the spec requires they be included.
+	 */
+	if (!json_iter_parse(&jsiter,
+			JSON_MANDATORY("name", JSON_STRING, NULL),
+			JSON_MANDATORY("wi-fi_tech", JSON_STRING, &tech),
+			JSON_MANDATORY("netRole", JSON_STRING, &role),
+			JSON_UNDEFINED))
+		goto configure_failure;
+
+	if (strcmp(tech, "infra"))
+		goto configure_failure;
+
+	if (strcmp(role, "sta"))
+		goto configure_failure;
+
+	json_contents_free(c);
+
+	memcpy(dpp->e_nonce, e_nonce, dpp->nonce_len);
+
+	dpp->state = DPP_STATE_CONFIGURING;
+
+	dpp_send_config_response(dpp, DPP_STATUS_OK);
+
+	return;
+
+configure_failure:
+	dpp_send_config_response(dpp, DPP_STATUS_CONFIGURE_FAILURE);
+	/*
+	 * The other peer is still authenticated, and can potentially send
+	 * additional requests so keep this session alive.
+	 */
+}
+
+static void dpp_handle_config_response(struct dpp_sm *dpp, const uint8_t *frame,
+					size_t frame_len)
+{
+	const uint8_t *attrs;
+	size_t attrs_len;
+	uint16_t status;
+	uint8_t diag_token;
+	uint16_t comeback;
+	struct dpp_attr_iter iter;
+	enum dpp_attribute_type type;
+	size_t len;
+	const uint8_t *data;
+	const char *json = NULL;
+	size_t json_len = 0;
+	int dstatus = -1;
+	const uint8_t *wrapped = NULL;
+	const uint8_t *e_nonce = NULL;
+	size_t wrapped_len = 0;
+	_auto_(l_free) uint8_t *unwrapped = NULL;
+
+	l_debug("config response");
+
+	attrs = dpp_parse_config_header(DPP_ACTION_GAS_RESPONSE, frame,
+					frame_len, &diag_token, &status,
+					&comeback, &attrs_len);
+	if (!attrs || diag_token != dpp->diag_token || status != 0) {
+		l_debug("failed to parse header");
+		return;
+	}
+
+	/* TODO: comeback delay */
+	if (comeback != 0) {
+		l_debug("comeback not zero");
+		return;
+	}
+
+	if (dpp->state != DPP_STATE_CONFIGURING) {
+		l_debug("state not configuring");
+		return;
+	}
+
+
+	dpp_attr_iter_init(&iter, attrs, attrs_len);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_STATUS:
+			dstatus = l_get_u8(data);
+			break;
+		case DPP_ATTR_WRAPPED_DATA:
+			wrapped = data;
+			wrapped_len = len;
+			break;
+		default:
+			/*
+			 * TODO: CSR Attribute
+			 */
+			break;
+		}
+	}
+
+	if (dstatus != DPP_STATUS_OK || !wrapped) {
+		l_debug("Bad status or missing attributes");
+		return;
+	}
+
+	unwrapped = dpp_unwrap_attr(attrs, wrapped - attrs - 4, NULL, 0, dpp->ke,
+					dpp->key_len, wrapped, wrapped_len,
+					&wrapped_len);
+	if (!unwrapped) {
+		l_debug("Failed to unwrap");
+		return;
+	}
+
+	dpp_attr_iter_init(&iter, unwrapped, wrapped_len);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_ENROLLEE_NONCE:
+			if (len != dpp->nonce_len)
+				break;
+
+			if (memcmp(data, dpp->e_nonce, dpp->nonce_len))
+				break;
+
+			e_nonce = data;
+			break;
+		case DPP_ATTR_CONFIGURATION_OBJECT:
+			json = (const char *)data;
+			json_len = len;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!json || !e_nonce) {
+		l_debug("No configuration object in response");
+		return;
+	}
+
+	dpp->config = dpp_parse_configuration_object(json, json_len);
+	if (!dpp->config) {
+		l_error("Configuration object did not parse");
+		return;
+	}
+
+	dpp_send_config_result(dpp);
+
+	l_timeout_remove(dpp->timeout);
+	dpp->timeout = NULL;
+
+	if (dpp->event_cb)
+		dpp->event_cb(DPP_EVENT_SUCCESS, NULL, dpp->user_data);
+}
+
+static void dpp_handle_config_result_frame(struct dpp_sm *dpp,
+						const uint8_t *frame,
+						size_t frame_len)
+{
+	struct dpp_attr_iter iter;
+	enum dpp_attribute_type type;
+	size_t len;
+	const uint8_t *data;
+	int status = -1;
+	const void *e_nonce = NULL;
+	const void *wrapped = NULL;
+	size_t wrapped_len;
+	_auto_(l_free) void *unwrapped = NULL;
+	const void *ad0 = frame;
+	const void *ad1 = frame + 6;
+
+	if (dpp->state != DPP_STATE_CONFIGURING)
+		return;
+
+	dpp_attr_iter_init(&iter, frame + 6, frame_len - 6);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_WRAPPED_DATA:
+			wrapped = data;
+			wrapped_len = len;
+			break;
+		default:
+			/* Wrapped data should be only attribute */
+			return;
+		}
+	}
+
+	if (!wrapped) {
+		l_debug("No wrapped data in config result");
+		return;
+	}
+
+	unwrapped = dpp_unwrap_attr(ad0, 6, ad1, wrapped - 4 - ad1,
+					dpp->ke, dpp->key_len, wrapped,
+					wrapped_len, &wrapped_len);
+	if (!unwrapped) {
+		l_debug("Failed to unwrap DPP configuration result");
+		return;
+	}
+
+	dpp_attr_iter_init(&iter, unwrapped, wrapped_len);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_STATUS:
+			status = l_get_u8(data);
+			break;
+		case DPP_ATTR_ENROLLEE_NONCE:
+			e_nonce = data;
+			break;
+		default:
+			break;
+		}
+	}
+
+	l_timeout_remove(dpp->timeout);
+	dpp->timeout = NULL;
+
+	if (!dpp->event_cb)
+		return;
+
+	if (status != DPP_STATUS_OK || !e_nonce)
+		dpp_failed(dpp);
+	else
+		dpp->event_cb(DPP_EVENT_SUCCESS, NULL, dpp->user_data);
+}
+
+static void dpp_pkex_bad_group(struct dpp_sm *dpp, uint16_t group)
+{
+	uint16_t own_group = l_ecc_curve_get_ike_group(dpp->curve);
+
+	/*
+	 * TODO: The spec allows group negotiation, but it is not yet
+	 *       implemented.
+	 */
+	if (!group)
+		return;
+	/*
+	 * Section 5.6.2
+	 * "If the Responder's offered group offers less security
+	 * than the Initiator's offered group, then the Initiator should
+	 * ignore this message"
+	 */
+	if (group < own_group) {
+		l_debug("Offered group %u is less secure, ignoring",
+				group);
+		return;
+	}
+	/*
+	 * Section 5.6.2
+	 * "If the Responder's offered group offers equivalent or better
+	 * security than the Initiator's offered group, then the
+	 * Initiator may choose to abort its original request and try
+	 * another exchange using the group offered by the Responder"
+	 */
+	if (group >= own_group) {
+		l_debug("Offered group %u is the same or more secure, "
+			" but group negotiation is not supported", group);
+		return;
+	}
+}
+
+static void dpp_pkex_bad_code(struct dpp_sm *dpp)
+{
+	_auto_(l_ecc_point_free) struct l_ecc_point *qr = NULL;
+
+	qr = dpp_derive_qr(dpp->curve, dpp->pkex_key, dpp->pkex_id,
+				dpp->mac_responder);
+	if (!qr || l_ecc_point_is_infinity(qr)) {
+		l_debug("Qr computed to zero, new code should be provisioned");
+		return;
+	}
+
+	l_debug("Qr computed successfully but responder indicated otherwise");
+}
+
+static void dpp_handle_pkex_exchange_response(struct dpp_sm *dpp,
+						const uint8_t *frame,
+						size_t frame_len)
+{
+	struct dpp_attr_iter iter;
+	enum dpp_attribute_type type;
+	size_t len;
+	const uint8_t *data;
+	const uint8_t *status = NULL;
+	uint8_t version = 0;
+	const void *identifier = NULL;
+	size_t identifier_len = 0;
+	const void *key = NULL;
+	size_t key_len = 0;
+	uint16_t group = 0;
+	_auto_(l_ecc_point_free) struct l_ecc_point *n = NULL;
+	_auto_(l_ecc_point_free) struct l_ecc_point *j = NULL;
+	_auto_(l_ecc_point_free) struct l_ecc_point *qr = NULL;
+	_auto_(l_ecc_point_free) struct l_ecc_point *k = NULL;
+
+	l_debug("PKEX exchange response");
+
+	if (dpp->state != DPP_STATE_PKEX_EXCHANGE)
+		return;
+
+	if (dpp->role != DPP_CAPABILITY_ENROLLEE)
+		return;
+
+	dpp_attr_iter_init(&iter, frame + 6, frame_len - 6);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_STATUS:
+			if (len != 1)
+				return;
+
+			status = data;
+			break;
+		case DPP_ATTR_PROTOCOL_VERSION:
+			if (len != 1)
+				return;
+
+			version = l_get_u8(data);
+			break;
+		case DPP_ATTR_CODE_IDENTIFIER:
+			identifier = data;
+			identifier_len = len;
+			break;
+		case DPP_ATTR_ENCRYPTED_KEY:
+			if (len != dpp->key_len * 2)
+				return;
+
+			key = data;
+			key_len = len;
+			break;
+		case DPP_ATTR_FINITE_CYCLIC_GROUP:
+			if (len != 2)
+				return;
+
+			group = l_get_le16(data);
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!status) {
+		l_debug("No status attribute, ignoring");
+		return;
+	}
+
+	if (!key) {
+		l_debug("No encrypted key, ignoring");
+		return;
+	}
+
+	if (*status != DPP_STATUS_OK)
+		goto handle_status;
+
+	if (dpp->pkex_id) {
+		if (!identifier || identifier_len != strlen(dpp->pkex_id) ||
+					memcmp(dpp->pkex_id, identifier,
+						identifier_len)) {
+			l_debug("mismatch identifier, ignoring");
+			return;
+		}
+	}
+
+	if (version && version != dpp->pkex_version) {
+		l_debug("PKEX version does not match, igoring");
+		return;
+	}
+
+	n = l_ecc_point_from_data(dpp->curve, L_ECC_POINT_TYPE_FULL,
+					key, key_len);
+	if (!n) {
+		l_debug("failed to parse peers encrypted key");
+		goto failed;
+	}
+
+	/*
+	 * TODO: PKEX protocol version 2 does not require the initiator or
+	 *       responder MAC addresses. If using protocol version 2 there will
+	 *       be no requirement for the encapsulating protocol to set this.
+	 *
+	 * Until then (and always for protocol 1) the encapsulating protocol
+	 * must set the peers address in the PEER_ACCEPTED callback for PKEX.
+	 */
+	if (dpp->event_cb)
+		dpp->event_cb(DPP_EVENT_PEER_ACCEPTED, NULL, dpp->user_data);
+
+	if (L_WARN_ON(!dpp->mac_responder))
+		goto failed;
+
+	qr = dpp_derive_qr(dpp->curve, dpp->pkex_key, dpp->pkex_id,
+				dpp->mac_responder);
+	if (!qr)
+		goto failed;
+
+	dpp->y_or_x = l_ecc_point_new(dpp->curve);
+
+	/* Y' = N - Qr */
+	l_ecc_point_inverse(qr);
+	l_ecc_point_add(dpp->y_or_x, n, qr);
+
+	/*
+	 * "The resulting ephemeral key, denoted Y’, is then checked whether it
+	 * is the point-at-infinity. If it is not valid, the protocol ends
+	 * unsuccessfully"
+	 */
+	if (l_ecc_point_is_infinity(dpp->y_or_x)) {
+		l_debug("Y' computed to infinity, failing");
+		goto failed;
+	}
+
+	k = l_ecc_point_new(dpp->curve);
+
+	/* K = Y' * x */
+	l_ecc_point_multiply(k, dpp->pkex_private, dpp->y_or_x);
+
+	dpp_derive_z(dpp->mac_initiator, dpp->mac_responder, n, dpp->pkex_m, k,
+				dpp->pkex_key, dpp->pkex_id,
+				dpp->z, &dpp->z_len);
+
+	/* J = a * Y' */
+	j = l_ecc_point_new(dpp->curve);
+
+	l_ecc_point_multiply(j, dpp->boot_private, dpp->y_or_x);
+
+	if (!dpp_derive_u(j, dpp->mac_initiator, dpp->boot_public, dpp->y_or_x,
+				dpp->pkex_public, dpp->u, &dpp->u_len)) {
+		l_debug("failed to compute u");
+		goto failed;
+	}
+
+	dpp_send_pkex_commit_reveal_request(dpp);
+
+	return;
+
+handle_status:
+	switch (*status) {
+	case DPP_STATUS_BAD_GROUP:
+		dpp_pkex_bad_group(dpp, group);
+		break;
+	case DPP_STATUS_BAD_CODE:
+		dpp_pkex_bad_code(dpp);
+		break;
+	default:
+		l_debug("Unhandled status %u", *status);
+		break;
+	}
+
+failed:
+	dpp_failed(dpp);
+}
+
+static void dpp_handle_pkex_v1_exchange_request(struct dpp_sm *dpp,
+					const uint8_t *frame, size_t frame_len)
+{
+	struct dpp_attr_iter iter;
+	enum dpp_attribute_type type;
+	size_t len;
+	const uint8_t *data;
+	uint8_t version = 0;
+	uint16_t group = 0;
+	const void *id = NULL;
+	size_t id_len = 0;
+	const void *key = NULL;
+	size_t key_len = 0;
+	_auto_(l_ecc_point_free) struct l_ecc_point *m = NULL;
+
+	l_debug("PKEX exchange request");
+
+	if (dpp->state != DPP_STATE_PKEX_EXCHANGE)
+		return;
+
+	if (dpp->role != DPP_CAPABILITY_CONFIGURATOR)
+		return;
+
+	dpp_attr_iter_init(&iter, frame + 6, frame_len - 6);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_PROTOCOL_VERSION:
+			if (len != 1)
+				return;
+
+			version = l_get_u8(data);
+			break;
+		case DPP_ATTR_FINITE_CYCLIC_GROUP:
+			if (len != 2)
+				return;
+
+			group = l_get_le16(data);
+			break;
+		case DPP_ATTR_CODE_IDENTIFIER:
+			id = data;
+			id_len = len;
+			break;
+		case DPP_ATTR_ENCRYPTED_KEY:
+			key = data;
+			key_len = len;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!key || !group) {
+		l_debug("initiator did not provide group or key, ignoring");
+		return;
+	}
+
+	if (group != l_ecc_curve_get_ike_group(dpp->curve)) {
+		l_debug("initiator is not using the same group");
+		goto bad_group;
+	}
+
+	/*
+	 * If the group isn't the same the key length won't match, so check
+	 * this here after we've determined the groups are equal
+	 */
+	if (key_len != dpp->key_len * 2) {
+		l_debug("Unexpected encrypted key length");
+		return;
+	}
+
+	if (version && version != dpp->pkex_version) {
+		l_debug("initiator is not using the same version, ignoring");
+		return;
+	}
+
+	if (dpp->pkex_id) {
+		if (!id || id_len != strlen(dpp->pkex_id) ||
+				memcmp(dpp->pkex_id, id, id_len)) {
+			l_debug("mismatch identifier, ignoring");
+			return;
+		}
+	}
+
+	m = l_ecc_point_from_data(dpp->curve, L_ECC_POINT_TYPE_FULL,
+					key, key_len);
+	if (!m) {
+		l_debug("could not parse key from initiator, ignoring");
+		return;
+	}
+
+	if (dpp->event_cb)
+		dpp->event_cb(DPP_EVENT_PEER_ACCEPTED, NULL, dpp->user_data);
+
+	if (!dpp->pkex_key) {
+		/*
+		 * "If an optional code identifier is used, it shall be a UTF-8
+		 *  string not greater than eighty (80) octets"
+		 */
+		if (!id || id_len > 80 || !l_utf8_validate(id, id_len, NULL)) {
+			l_debug("Configurator started with agent but enrollee "
+				"sent invalid or no identifier, ignoring");
+			return;
+		}
+
+		dpp->pkex_id = l_strndup(id, id_len);
+
+		if (dpp->event_cb)
+			dpp->event_cb(DPP_EVENT_PKEX_KEY_REQUESTED,
+					dpp->pkex_id, dpp->user_data);
+
+		/* Save the encrypted key/identifier for the agent callback */
+		dpp->peer_encr_key = l_steal_ptr(m);
+
+		return;
+	}
+
+	dpp_process_pkex_exchange_request(dpp, m);
+
+	return;
+
+bad_group:
+	dpp_send_pkex_bad_group(dpp);
+}
+
+static void dpp_handle_pkex_commit_reveal_request(struct dpp_sm *dpp,
+					const uint8_t *frame, size_t frame_len)
+{
+	struct dpp_attr_iter iter;
+	enum dpp_attribute_type type;
+	size_t len;
+	const uint8_t *data;
+	const void *wrapped = NULL;
+	size_t wrapped_len = 0;
+	_auto_(l_free) uint8_t *unwrapped = NULL;
+	size_t unwrapped_len;
+	uint8_t zero = 0;
+	const void *key = 0;
+	size_t key_len = 0;
+	const void *i_auth = NULL;
+	size_t i_auth_len = 0;
+	_auto_(l_ecc_point_free) struct l_ecc_point *j = NULL;
+	_auto_(l_ecc_point_free) struct l_ecc_point *l = NULL;
+	_auto_(l_ecc_point_free) struct l_ecc_point *peer_public = NULL;
+	uint8_t u[L_ECC_SCALAR_MAX_BYTES];
+	size_t u_len = 0;
+	uint8_t v[L_ECC_SCALAR_MAX_BYTES];
+	size_t v_len = 0;
+
+	l_debug("PKEX commit-reveal request");
+
+	if (dpp->state != DPP_STATE_PKEX_COMMIT_REVEAL)
+		return;
+
+	if (dpp->role != DPP_CAPABILITY_CONFIGURATOR)
+		return;
+
+	dpp_attr_iter_init(&iter, frame + 6, frame_len - 6);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_WRAPPED_DATA:
+			wrapped = data;
+			wrapped_len = len;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!wrapped) {
+		l_debug("No wrapped data");
+		return;
+	}
+
+	unwrapped = dpp_unwrap_attr(frame, 6, &zero, 1, dpp->z, dpp->z_len,
+					wrapped, wrapped_len, &unwrapped_len);
+	if (!unwrapped) {
+		l_debug("Failed to unwrap attributes");
+		return;
+	}
+
+	dpp_attr_iter_init(&iter, unwrapped, unwrapped_len);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_BOOTSTRAPPING_KEY:
+			if (len != dpp->key_len * 2)
+				return;
+
+			key = data;
+			key_len = len;
+			break;
+		case DPP_ATTR_INITIATOR_AUTH_TAG:
+			if (len != 32)
+				return;
+
+			i_auth = data;
+			i_auth_len = len;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!key || !i_auth) {
+		l_debug("missing attributes");
+		return;
+	}
+
+	peer_public = l_ecc_point_from_data(dpp->curve,
+					L_ECC_POINT_TYPE_FULL, key, key_len);
+	if (!peer_public) {
+		l_debug("peers boostrapping key did not validate");
+		goto failed;
+	}
+
+	/* J' = y * A' */
+	j = l_ecc_point_new(dpp->curve);
+
+	l_ecc_point_multiply(j, dpp->pkex_private, peer_public);
+
+	if (!dpp_derive_u(j, dpp->mac_initiator, peer_public,
+			dpp->pkex_public, dpp->y_or_x, u, &u_len)) {
+		l_debug("Failed to derive u");
+		goto failed;
+	}
+
+	if (memcmp(u, i_auth, i_auth_len)) {
+		l_debug("Initiator auth tag did not verify");
+		goto failed;
+	}
+
+	/* L' = x * B' */
+	l = l_ecc_point_new(dpp->curve);
+
+	l_ecc_point_multiply(l, dpp->boot_private, dpp->y_or_x);
+
+	if (!dpp_derive_v(l, dpp->mac_responder, dpp->boot_public, dpp->y_or_x,
+				dpp->pkex_public, v, &v_len)) {
+		l_debug("Failed to derive v");
+		goto failed;
+	}
+
+	dpp_sm_set_peer_bootstrap(dpp, peer_public);
+
+	dpp_send_pkex_commit_reveal_response(dpp, v, v_len);
+
+	dpp->mutual_auth = true;
+	dpp_start_authentication(dpp);
+
+	return;
+
+failed:
+	dpp_failed(dpp);
+}
+
+static void dpp_handle_pkex_commit_reveal_response(struct dpp_sm *dpp,
+					const uint8_t *frame, size_t frame_len)
+{
+	struct dpp_attr_iter iter;
+	enum dpp_attribute_type type;
+	size_t len;
+	const uint8_t *data;
+	const uint8_t *wrapped = NULL;
+	size_t wrapped_len = 0;
+	uint8_t one = 1;
+	_auto_(l_free) uint8_t *unwrapped = NULL;
+	size_t unwrapped_len = 0;
+	const uint8_t *boot_key = NULL;
+	size_t boot_key_len = 0;
+	const uint8_t *r_auth = NULL;
+	uint8_t v[L_ECC_SCALAR_MAX_BYTES];
+	size_t v_len;
+	_auto_(l_ecc_point_free) struct l_ecc_point *l = NULL;
+	_auto_(l_ecc_point_free) struct l_ecc_point *peer_boot = NULL;
+
+	l_debug("PKEX commit reveal response");
+
+	if (dpp->state != DPP_STATE_PKEX_COMMIT_REVEAL)
+		return;
+
+	if (dpp->role != DPP_CAPABILITY_ENROLLEE)
+		return;
+
+	dpp_attr_iter_init(&iter, frame + 6, frame_len - 6);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_WRAPPED_DATA:
+			wrapped = data;
+			wrapped_len = len;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (!wrapped) {
+		l_debug("No wrapped data");
+		return;
+	}
+
+	unwrapped = dpp_unwrap_attr(frame, 6, &one, 1, dpp->z, dpp->z_len,
+					wrapped, wrapped_len, &unwrapped_len);
+	if (!unwrapped) {
+		l_debug("Failed to unwrap Reveal-Commit message");
+		return;
+	}
+
+	dpp_attr_iter_init(&iter, unwrapped, unwrapped_len);
+
+	while (dpp_attr_iter_next(&iter, &type, &len, &data)) {
+		switch (type) {
+		case DPP_ATTR_BOOTSTRAPPING_KEY:
+			if (len != dpp->key_len * 2)
+				return;
+
+			boot_key = data;
+			boot_key_len = len;
+			break;
+		case DPP_ATTR_RESPONDER_AUTH_TAG:
+			if (len != 32)
+				return;
+
+			r_auth = data;
+			break;
+		default:
+			break;
+		}
+	}
+
+	peer_boot = l_ecc_point_from_data(dpp->curve, L_ECC_POINT_TYPE_FULL,
+							boot_key, boot_key_len);
+	if (!peer_boot) {
+		l_debug("Peer public bootstrapping key was invalid");
+		goto failed;
+	}
+
+	/* L = b * X' */
+	l = l_ecc_point_new(dpp->curve);
+
+	l_ecc_point_multiply(l, dpp->pkex_private, peer_boot);
+
+	if (!dpp_derive_v(l, dpp->mac_responder, peer_boot,
+				dpp->pkex_public, dpp->y_or_x, v, &v_len)) {
+		l_debug("Failed to derive v");
+		goto failed;
+	}
+
+	if (memcmp(v, r_auth, v_len)) {
+		l_debug("Bootstrapping data did not verify");
+		goto failed;
+	}
+
+	dpp_sm_set_peer_bootstrap(dpp, peer_boot);
+
+	dpp->mutual_auth = true;
+	dpp_start_authentication(dpp);
+
+	return;
+
+failed:
+	dpp_failed(dpp);
+}
+
+static bool dpp_validate_header(const uint8_t *data, size_t len,
+					enum dpp_frame_type *type)
+{
+	/* Ensure header + message type */
+	if (len < 6)
+		return false;
+
+	if (memcmp(data, wifi_alliance_oui, 3))
+		return false;
+
+	/* WiFi Allicance DPP OI type */
+	if (data[3] != 0x1a)
+		return false;
+
+	/* Cryptosuite */
+	if (data[4] != 1)
+		return false;
+
+	if (type)
+		*type = data[5];
+
+	return true;
+}
+
+static void dpp_handle_frame(struct dpp_sm *dpp,
+				const uint8_t *data, size_t len)
+{
+	enum dpp_frame_type type;
+
+	if (!dpp_validate_header(data, len, &type)) {
+		l_debug("header did not validate");
+		return;
+	}
+
+	switch (type) {
+	case DPP_FRAME_AUTHENTICATION_REQUEST:
+		dpp_handle_auth_request(dpp, data, len);
+		break;
+	case DPP_FRAME_AUTHENTICATION_RESPONSE:
+		dpp_handle_auth_response(dpp, data, len);
+		break;
+	case DPP_FRAME_AUTHENTICATION_CONFIRM:
+		dpp_handle_auth_confirm(dpp, data, len);
+		break;
+	case DPP_FRAME_CONFIGURATION_RESULT:
+		dpp_handle_config_result_frame(dpp, data, len);
+		break;
+	case DPP_FRAME_PRESENCE_ANNOUNCEMENT:
+		dpp_handle_presence_announcement(dpp, data, len);
+		break;
+	case DPP_FRAME_PKEX_XCHG_RESPONSE:
+		dpp_handle_pkex_exchange_response(dpp, data, len);
+		break;
+	case DPP_FRAME_PKEX_COMMIT_REVEAL_RESPONSE:
+		dpp_handle_pkex_commit_reveal_response(dpp, data, len);
+		break;
+	case DPP_FRAME_PKEX_VERSION1_XCHG_REQUEST:
+		dpp_handle_pkex_v1_exchange_request(dpp, data, len);
+		break;
+	case DPP_FRAME_PKEX_COMMIT_REVEAL_REQUEST:
+		dpp_handle_pkex_commit_reveal_request(dpp, data, len);
+		break;
+	default:
+		l_debug("Unhandled DPP frame %u", type);
+		break;
+	}
+}
+
+void dpp_handle_rx(struct dpp_sm *dpp, const uint8_t *data, size_t len)
+{
+	if (len < 1)
+		return;
+
+	switch (*data) {
+	case DPP_ACTION_VENDOR_SPECIFIC:
+		dpp_handle_frame(dpp, data + 1, len - 1);
+		break;
+	/* For the GAS frames save the action byte to validate the frame */
+	case DPP_ACTION_GAS_REQUEST:
+		dpp_handle_config_request(dpp, data + 1, len - 1);
+		break;
+	case DPP_ACTION_GAS_RESPONSE:
+		dpp_handle_config_response(dpp, data + 1, len - 1);
+		break;
+	default:
+		break;
+	}
 }
