Skip to content

Commit

Permalink
Initial implementation of DPP2-based provisioning/reprovisioning (v1.…
Browse files Browse the repository at this point in the history
…0.65)

Added logic to the ifupdown.sh script to enable wifi monitoring mode
 This is a temporary work-around to enable the forwarding of DPP Presence Announcements
 from the wifi driver into hostapd. The driver-level commands to enable the forwarding of
 these wifi broadcast packets (NL80211_EXT_FEATURE_MULTICAST_REGISTRATIONS) in the
 ATH9K driver isn't currently included in a lot of kernel distros (for Rasp Pi, kernel
 version 5.10+ is required)

 The deice "onboard" endpoint now takes on different behavior when the
 URI contains a "V:2" element - preparing hostapd to process a DPP
 Presence Announcment ("chirp").

Added "reconfigure" device endpont and associated implementation.
  • Loading branch information
craigpratt committed Nov 27, 2020
1 parent ec491a5 commit cd16295
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 31 deletions.
2 changes: 1 addition & 1 deletion distribution/Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.PHONY: all clean distclean deb scp

VERSION=1.0.64
VERSION=1.0.65
DIST=U18.04
FBASE=micronets-gw
SDIRS=../filesystem
Expand Down
19 changes: 19 additions & 0 deletions filesystem/opt/micronets-gw/bin/ifupdown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,23 @@ config_iptables_bridge_uplink() {
/sbin/sysctl -w net.ipv4.ip_forward=1
}

# This is a temporary work-around to enable the forwarding of DPP Presence Announcements
# from the wifi driver into hostapd. The driver-level commands to enable the forwarding of
# these wifi broadcast packets (NL80211_EXT_FEATURE_MULTICAST_REGISTRATIONS) in the
# ATH9K driver isn't currently included in a lot of kernel distros (for Rasp Pi, kernel
# version 5.10+ is required)
enable_wifi_monitor_mode() {
WIPHY_DEV=phy0
WIPHY_MON_DEV=wifimon0
if ifconfig $WIPHY_MON_DEV > /dev/null; then
debug_log "Monitor mode already enabled on $WIPHY_DEV (monitor device $WIPHY_MON_DEV)"
else
debug_log "Enabling monitor mode on $WIPHY_DEV (monitor device $WIPHY_MON_DEV)"
iw phy $WIPHY_DEV interface add $WIPHY_MON_DEV type monitor
ifconfig $WIPHY_MON_DEV up
fi
}

dump_iptables() {
iptables -L -v
iptables -L -v --table nat
Expand Down Expand Up @@ -93,6 +110,8 @@ else
debug_log "completed /etc/init.d/openvswitch-switch start."
fi

enable_wifi_monitor_mode

if [ "${MODE}" = "start" ]; then
eval OVS_EXTRA=\"${IF_OVS_EXTRA}\"

Expand Down
111 changes: 84 additions & 27 deletions micronets-gw-service/app/dpp_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,36 +111,93 @@ async def send_dpp_onboard_event_delayed(event_name, delay, reason=None, termina
else:
psk = None
passphrase = dev_psk
dpp_auth_init_cmd = HostapdAdapter.DPPAuthInitCommand(self.dpp_configurator_id, qrcode_id, self.ssid,
akms, psk=psk, passphrase=passphrase, freq=self.freq)

self.pending_onboard = {"micronet":micronet, "device": device, "onboard_params": onboard_params}
asyncio.ensure_future(self.send_dpp_onboard_event(micronet, device, DPPHandler.EVENT_ONBOARDING_STARTED,
f"DPP Started (issuing \"{dpp_auth_init_cmd.get_command_string()}\")"))
await self.hostapd_adapter.send_command(dpp_auth_init_cmd)
result = await dpp_auth_init_cmd.get_response()
logger.info(f"{__name__}: Auth Init result: {result}")

async def onboard_timeout_handler():
await asyncio.sleep(DPPHandler.DPP_ONBOARD_TIMEOUT_S)
if self.pending_onboard:
logger.info(f"{__name__}: Onboarding TIMED OUT (after {DPPHandler.DPP_ONBOARD_TIMEOUT_S} seconds)")
pend_micronet = self.pending_onboard['micronet']
pend_device = self.pending_onboard['device']
await self.send_dpp_onboard_event(pend_micronet, pend_device,
DPPHandler.EVENT_ONBOARDING_FAILED,
f"Onboarding timed out (after {DPPHandler.DPP_ONBOARD_TIMEOUT_S} seconds)")
self.pending_onboard = None

if ";V:2;" in qrcode_uri:
# Perform DPP V2 onboarding (set bootstrapping info for DPP Chirp/Presence Announcement
dpp_bootstrap_set_cmd = HostapdAdapter.DPPBootstrapSet(self.dpp_configurator_id, qrcode_id, self.ssid,
akms, psk=psk, passphrase=passphrase)
await self.hostapd_adapter.send_command(dpp_bootstrap_set_cmd)
result = await dpp_bootstrap_set_cmd.get_response()
logger.info(f"{__name__}: Bootstrap Set result: {result}")
if await dpp_bootstrap_set_cmd.was_successful():
logger.info(f"{__name__}: Successfully set credentials for URI {qrcode_uri}")
return '', 200
else:
logger.info(f"{__name__}: Could not set credentials for URI {qrcode_uri}")
return f"Could not set DPP V2 credentials for given URI ({result})", 400
else:
# Perform DPP V1 onboarding (send Auth Init)
dpp_auth_init_cmd = HostapdAdapter.DPPAuthInitCommand(self.dpp_configurator_id, qrcode_id, self.ssid,
akms, psk=psk, passphrase=passphrase, freq=self.freq)

self.pending_onboard = {"micronet":micronet, "device": device, "onboard_params": onboard_params}
asyncio.ensure_future(self.send_dpp_onboard_event(micronet, device, DPPHandler.EVENT_ONBOARDING_STARTED,
f"DPP Started (issuing \"{dpp_auth_init_cmd.get_command_string()}\")"))
await self.hostapd_adapter.send_command(dpp_auth_init_cmd)
result = await dpp_auth_init_cmd.get_response()
logger.info(f"{__name__}: Auth Init result: {result}")

async def onboard_timeout_handler():
await asyncio.sleep(DPPHandler.DPP_ONBOARD_TIMEOUT_S)
if self.pending_onboard:
logger.info(f"{__name__}: Onboarding TIMED OUT (after {DPPHandler.DPP_ONBOARD_TIMEOUT_S} seconds)")
pend_micronet = self.pending_onboard['micronet']
pend_device = self.pending_onboard['device']
await self.send_dpp_onboard_event(pend_micronet, pend_device,
DPPHandler.EVENT_ONBOARDING_FAILED,
f"Onboarding timed out (after {DPPHandler.DPP_ONBOARD_TIMEOUT_S} seconds)")
self.pending_onboard = None
else:
logger.info(f"{__name__}: Onboarding completed before timeout (< {DPPHandler.DPP_ONBOARD_TIMEOUT_S} seconds)")

if await dpp_auth_init_cmd.was_successful():
self.pending_timeout_task = asyncio.ensure_future(onboard_timeout_handler())
else:
logger.info(f"{__name__}: Onboarding completed before timeout (< {DPPHandler.DPP_ONBOARD_TIMEOUT_S} seconds)")
asyncio.ensure_future(self.send_dpp_onboard_event(micronet, device, DPPHandler.EVENT_ONBOARDING_FAILED,
f"DPP Authorization failed {dpp_auth_init_cmd.get_command_string()} returned: ({result})"))
self.pending_onboard = None
return '', 200

async def reprovision_device(self, micronet_id, device_id,):
logger.info(f"DPPHandler.reprovision_device(micronet '{micronet_id}', device '{device_id}'')")
conf_model = get_conf_model()

# Make sure an pending updates have been processed
await conf_model.update_conf_now()

micronet = conf_model.check_micronet_reference(micronet_id)
device = conf_model.check_device_reference(micronet_id, device_id)

if not self.hostapd_adapter.is_cli_connected():
return "Hostapd CLI is not connected", 500

if not self.hostapd_adapter.is_cli_ready():
return "Hostapd CLI is not ready (hostapd is probably not running)", 500

logger.info(f"DPPHandler.onboard_device: Issuing DPP reprovisioning commands for device '{device_id}' in micronet '{micronet_id}...")

dev_psk = device.get("psk")
# For now, the psk field is dual-purpose. For WPA2, a <64char "psk" will be converted into a PSK internally
# So send it through as a PSK and a passphrase
if len(dev_psk) == 64:
psk = dev_psk
passphrase = None
else:
psk = None
passphrase = dev_psk

if await dpp_auth_init_cmd.was_successful():
self.pending_timeout_task = asyncio.ensure_future(onboard_timeout_handler())
dpp_reprovision_params_cmd = HostapdAdapter.DPPSetDPPConfigParamsCommand(self.dpp_configurator_id, self.ssid,
["psk"], psk=psk, passphrase=passphrase)
await self.hostapd_adapter.send_command(dpp_reprovision_params_cmd)
result = await dpp_reprovision_params_cmd.get_response()
logger.info(f"{__name__}: Result of setting reprovision params: {result}")

if await dpp_reprovision_params_cmd.was_successful():
logger.info(f"{__name__}: Successfully set reprovisioning credentials for micronet/device {micronet_id}/{device_id}")
return '', 200
else:
asyncio.ensure_future(self.send_dpp_onboard_event(micronet, device, DPPHandler.EVENT_ONBOARDING_FAILED,
f"DPP Authorization failed {dpp_auth_init_cmd.get_command_string()} returned: ({result})"))
self.pending_onboard = None
return '', 200
logger.info(f"{__name__}: Failed to set reprovisioning credentials for micronet/device {micronet_id}/{device_id}")
return f"Could not set DPP V2 reprovisioning credentials for micronet/device {micronet_id}/{device_id}", 400

async def handle_hostapd_cli_event(self, event):
logger.info(f"DPPHandler.handle_hostapd_cli_event({event})")
Expand Down
11 changes: 9 additions & 2 deletions micronets-gw-service/app/gateway_service_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,15 +380,22 @@ async def onboard_device (micronet_id, device_id):
check_micronet_id (micronet_id, request.path)
check_device_id (device_id, request.path)
top_level = await request.get_json ()
logger.info(f"onboad_device: top_level: {top_level}")
logger.info(f"onboard_device: top_level: {top_level}")
check_for_unrecognized_entries (top_level, ['dpp'])
dpp_obj = top_level['dpp']
logger.info(f"onboad_device: dpp_obj: {dpp_obj}")
check_for_unrecognized_entries(dpp_obj, ['uri','akms'])
uri = check_field (dpp_obj, 'uri', str, True)
akms = check_akms (dpp_obj, 'akms', True)
return await get_dpp_handler().onboard_device (micronet_id, device_id, top_level)

@app.route (api_prefix + '/micronets/<micronet_id>/devices/<device_id>/reconfigure', methods=['PUT'])
async def reconfig_device (micronet_id, device_id):
micronet_id = micronet_id.lower ()
device_id = device_id.lower ()
check_micronet_id (micronet_id, request.path)
check_device_id (device_id, request.path)
return await get_dpp_handler().reprovision_device (micronet_id, device_id)

valid_akms = ("psk", "dpp", "sae")

def check_akms (container, field_name, required):
Expand Down
107 changes: 106 additions & 1 deletion micronets-gw-service/app/hostapd_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,6 @@ async def send_command(self, command):

return command


class PingCLICommand(HostapdCLICommand):
def __init__ (self, event_loop=asyncio.get_event_loop()):
super().__init__(event_loop)
Expand Down Expand Up @@ -483,6 +482,112 @@ async def was_successful(self):
await self.get_response()
return self.success

class DPPBootstrapSet(HostapdCLICommand):
def __init__ (self, configurator_id, qrcode_id, ssid, akms, psk=None, passphrase=None,
event_loop=asyncio.get_event_loop()):
super().__init__(event_loop)
self.configurator_id = configurator_id
self.qrcode_id = qrcode_id
self.ssid = ssid
self.psk = psk
self.passphrase = passphrase
self.akms = akms
self.success = False
self.passphrase_asciihex = None

def get_command_string(self):
ssid_asciihex = self.ssid.encode("ascii").hex()

# dpp_bootstrap_set 1 conf=sta-psk ssid=<enc ssid> psk=<64 hex chars) configurator=1 conn_status=0 group_id=micronet-01
cmd = f"dpp_bootstrap_set {self.qrcode_id} ssid={ssid_asciihex} configurator={self.configurator_id}"

# Currently allowed configs: psk, sae, dpp, psk+sae, dpp+sae, dpp+psk+sae
# (see dpp_configuration_alloc in src/common/dpp.c of hostap sources)
akm_str = ""
if 'dpp' in self.akms:
akm_str += "+dpp"
if 'psk' in self.akms:
if not (self.psk or self.passphrase):
raise Exception(f"'psk' included in AKMS but no PSK or passphrase provided")
akm_str += "+psk"
if 'sae' in self.akms:
if not self.passphrase:
raise Exception(f"'sae' included in AKMS but no passphrase provided")
akm_str += "+sae"
if len(akm_str) == 0:
raise Exception(f"No valid akms elements found (akms: {self.akms})")
# Note: akm_str will have an extra "+" at the front
cmd += f" conf=sta-{akm_str[1:]}"

if self.psk:
cmd += f" psk={self.psk}"
if self.passphrase:
passphrase_asciihex = self.passphrase.encode("ascii").hex()
cmd += f" pass={passphrase_asciihex}"
return cmd

async def process_response_data(self, response):
try:
self.success = "OK" in response
finally:
await super().process_response_data(response)

async def was_successful(self):
await self.get_response()
return self.success

class DPPSetDPPConfigParamsCommand(HostapdCLICommand):
def __init__ (self, configurator_id, ssid, akms, psk=None, passphrase=None,
event_loop=asyncio.get_event_loop()):
super().__init__(event_loop)
self.configurator_id = configurator_id
self.ssid = ssid
self.psk = psk
self.passphrase = passphrase
self.akms = akms
self.success = False

def get_command_string(self):
ssid_asciihex = self.ssid.encode("ascii").hex()
# set dpp_configurator_params "conf=sta-psk ssid=<encoded-ssid> pass=<hex-encoded pass> conn_status=0 group_id=micronet-01"
cmd = f"set dpp_configurator_params \"conn_status=0 ssid={ssid_asciihex}"

# Currently allowed configs: psk, sae, dpp, psk+sae, dpp+sae, dpp+psk+sae
# (see dpp_configuration_alloc in src/common/dpp.c of hostap sources)
akm_str = ""
if 'dpp' in self.akms:
akm_str += "+dpp"
if 'psk' in self.akms:
if not (self.psk or self.passphrase):
raise Exception(f"'psk' included in AKMS but no PSK or passphrase provided")
akm_str += "+psk"
if 'sae' in self.akms:
if not self.passphrase:
raise Exception(f"'sae' included in AKMS but no passphrase provided")
akm_str += "+sae"
if len(akm_str) == 0:
raise Exception(f"No valid akms elements found (akms: {self.akms})")
# Note: akm_str will have an extra "+" at the front
cmd += f" conf=sta-{akm_str[1:]}"

if self.psk:
cmd += f" psk={self.psk}"
if self.passphrase:
passphrase_asciihex = self.passphrase.encode("ascii").hex()
cmd += f" pass={passphrase_asciihex}"
cmd += '"'
return cmd

async def process_response_data(self, response):
try:
self.success = "OK" in response
finally:
await super().process_response_data(response)

async def was_successful(self):
await self.get_response()
return self.success

class ReloadPSKCLICommand(HostapdCLICommand):
def __init__ (self, event_loop=asyncio.get_event_loop()):
super().__init__(event_loop)
Expand Down
3 changes: 3 additions & 0 deletions micronets-gw-service/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ class WirelessGatewayDebugConfig (WirelessGatewayConfig):
LOGGING_LEVEL = logging.DEBUG
LOGFILE_MODE = 'w' # clears the log at startup

class WirelessGatewayDebugConfigNoLogfile (WirelessGatewayDebugConfig):
LOGFILE_PATH = None

class WirelessGatewayDebugConfigWithWebsocket (WirelessGatewayDebugConfig, WirelessGatewayConfigWithWebsocket):
pass

Expand Down

0 comments on commit cd16295

Please sign in to comment.