Skip to content

Commit 86b1ee3

Browse files
committed
Add: Apple/Google BLE Exposure Notification Service (v1.2)
1 parent 32211b4 commit 86b1ee3

File tree

2 files changed

+127
-0
lines changed

2 files changed

+127
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# -*- mode: python3; indent-tabs-mode: nil; tab-width: 4 -*-
2+
# exposure_notification.py - Apple/Google Exposure Notification System
3+
#
4+
# This file is part of Scapy
5+
# See http://www.secdev.org/projects/scapy for more information
6+
# Copyright (C) 2020 Michael Farrell <[email protected]>
7+
# This program is published under a GPLv2 (or later) license
8+
#
9+
# scapy.contrib.description = Apple/Google Exposure Notification System (ENS)
10+
# scapy.contrib.status = loads
11+
"""
12+
Apple/Google Exposure Notification System (ENS), formerly known as
13+
Privacy-Preserving Contact Tracing Project.
14+
15+
This module parses the Bluetooth Low Energy beacon payloads used by the system.
16+
This does **not** yet implement any cryptographic functionality.
17+
18+
More info:
19+
20+
* `Apple: Privacy-Preserving Contact Tracing`__
21+
* `Google: Exposure Notifications`__
22+
* `Wikipedia: Exposure Notification`__
23+
24+
__ https://www.apple.com/covid19/contacttracing/
25+
__ https://www.google.com/covid19/exposurenotifications/
26+
__ https://en.wikipedia.org/wiki/Exposure_Notification
27+
28+
Bluetooth protocol specifications:
29+
30+
* `v1.1`_ (April 2020)
31+
* `v1.2`_ (April 2020)
32+
33+
.. _v1.1: https://blog.google/documents/58/Contact_Tracing_-_Bluetooth_Specification_v1.1_RYGZbKW.pdf
34+
.. _v1.2: https://covid19-static.cdn-apple.com/applications/covid19/current/static/contact-tracing/pdf/ExposureNotification-BluetoothSpecificationv1.2.pdf
35+
""" # noqa: E501
36+
37+
from scapy.fields import StrFixedLenField
38+
from scapy.layers.bluetooth import EIR_Hdr, EIR_ServiceData16BitUUID, \
39+
EIR_CompleteList16BitServiceUUIDs, LowEnergyBeaconHelper
40+
from scapy.packet import bind_layers, Packet
41+
42+
43+
EXPOSURE_NOTIFICATION_UUID = 0xFD6F
44+
45+
46+
class Exposure_Notification_Frame(Packet, LowEnergyBeaconHelper):
47+
"""Apple/Google BLE Exposure Notification broadcast frame."""
48+
name = "Exposure Notification broadcast"
49+
50+
fields_desc = [
51+
# Rolling Proximity Identifier
52+
StrFixedLenField("identifier", None, 16),
53+
# Associated Encrypted Metadata (added in v1.2)
54+
StrFixedLenField("metadata", None, 4),
55+
]
56+
57+
def build_eir(self):
58+
"""Builds a list of EIR messages to wrap this frame."""
59+
60+
return LowEnergyBeaconHelper.base_eir + [
61+
EIR_Hdr() / EIR_CompleteList16BitServiceUUIDs(svc_uuids=[
62+
EXPOSURE_NOTIFICATION_UUID]),
63+
EIR_Hdr() / EIR_ServiceData16BitUUID() / self
64+
]
65+
66+
67+
bind_layers(EIR_ServiceData16BitUUID, Exposure_Notification_Frame,
68+
svc_uuid=EXPOSURE_NOTIFICATION_UUID)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
% Exposure Notification System tests
2+
#
3+
# Type the following command to launch start the tests:
4+
# $ test/run_tests -P "load_contrib('exposure_notification')" -t test/contrib/exposure_notification.uts
5+
6+
+ ENS tests
7+
8+
= Setup
9+
10+
def next_eir(p):
11+
return EIR_Hdr(p[Padding].load)
12+
13+
= Presence check
14+
15+
Exposure_Notification_Frame
16+
17+
= Raw payload copied from BluetoothExplorer.app
18+
19+
d = hex_bytes('17df1d67405e3395470e62ca4fda6a9303687b31')
20+
p = Exposure_Notification_Frame(d)
21+
22+
assert p.identifier == hex_bytes('17df1d67405e3395470e62ca4fda6a93')
23+
assert p.metadata == hex_bytes('03687b31')
24+
25+
= Raw captured payload
26+
27+
d = hex_bytes('02011a03036ffd17166ffde23f352fa09307a85d4194912443180d484dc151')
28+
p = EIR_Hdr(d)
29+
30+
# First is a flags header
31+
assert EIR_Flags in p
32+
33+
# Then the 16-bit Service Class ID
34+
p = next_eir(p)
35+
assert p[EIR_CompleteList16BitServiceUUIDs].svc_uuids == [
36+
EXPOSURE_NOTIFICATION_UUID]
37+
38+
# Then the ENS
39+
p = next_eir(p)
40+
assert p[EIR_ServiceData16BitUUID].svc_uuid == EXPOSURE_NOTIFICATION_UUID
41+
assert p[Exposure_Notification_Frame].identifier == hex_bytes(
42+
'e23f352fa09307a85d4194912443180d')
43+
assert p[Exposure_Notification_Frame].metadata == hex_bytes('484dc151')
44+
45+
# Rebuild the payload.
46+
p2 = p[Exposure_Notification_Frame].build_eir()
47+
48+
# Our captured payload was from a mobile phone, but build_eir presumes that
49+
# we're broadcasting as an non-connectable, LE-only beacon. We need to adjust
50+
# these flags to match the captured packet.
51+
p2[0] = EIR_Hdr() / EIR_Flags(flags=[
52+
'general_disc_mode', 'simul_le_br_edr_ctrl', 'simul_le_br_edr_host'])
53+
54+
# Ensure we didn't mutate LowEnergyBeaconHelper.base_eir just then.
55+
assert LowEnergyBeaconHelper.base_eir[0][EIR_Flags].flags == [
56+
'general_disc_mode', 'br_edr_not_supported']
57+
58+
# Assemble all packet bytes
59+
assert b''.join(map(raw, p2)) == d

0 commit comments

Comments
 (0)