Merge lp:~mpontillo/maas/beaconing-packet-format into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 6099
Proposed branch: lp:~mpontillo/maas/beaconing-packet-format
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 673 lines (+345/-74)
4 files modified
src/provisioningserver/security.py (+31/-9)
src/provisioningserver/tests/test_security.py (+55/-44)
src/provisioningserver/utils/beaconing.py (+135/-10)
src/provisioningserver/utils/tests/test_beaconing.py (+124/-11)
To merge this branch: bzr merge lp:~mpontillo/maas/beaconing-packet-format
Reviewer Review Type Date Requested Status
Данило Шеган (community) Approve
Review via email: mp+325601@code.launchpad.net

Commit message

Add methods to encode and decode beacon packets.

Also, change the name of the Fernet methods to make it clear that the encryption is done using a pre-shared key (the MAAS shared secret).

Minor test suite refactoring for SharedSecretTestCase to make it more general.

To post a comment you must log in.
Revision history for this message
Данило Шеган (danilo) wrote :

I do have a few questions inline. Looks generally good, though.

review: Needs Information
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Thanks for the review. Some replies below. You've given me some things to think about, so I'll make this WIP for now.

Revision history for this message
Данило Шеган (danilo) wrote :

Looks great, thanks for the changes!

A few more things that you probably forgot to complete with it being past 1am :-)

Not blocking since the gist of the stuff is there (basically, you are not making use of the new exception and raw=True parameter).

review: Approve
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Thanks much for the review; some replies below.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/provisioningserver/security.py'
--- src/provisioningserver/security.py 2017-06-02 17:28:04 +0000
+++ src/provisioningserver/security.py 2017-06-21 15:18:45 +0000
@@ -9,7 +9,10 @@
9 "get_shared_secret_from_filesystem",9 "get_shared_secret_from_filesystem",
10]10]
1111
12from base64 import urlsafe_b64encode12from base64 import (
13 urlsafe_b64decode,
14 urlsafe_b64encode,
15)
13import binascii16import binascii
14from binascii import (17from binascii import (
15 a2b_hex,18 a2b_hex,
@@ -41,6 +44,10 @@
41)44)
4245
4346
47class MissingSharedSecret(RuntimeError):
48 """Raised when the MAAS shared secret is missing."""
49
50
44def to_hex(b):51def to_hex(b):
45 """Convert byte string to hex encoding."""52 """Convert byte string to hex encoding."""
46 assert isinstance(b, bytes), "%r is not a byte string" % (b,)53 assert isinstance(b, bytes), "%r is not a byte string" % (b,)
@@ -140,7 +147,8 @@
140 global _fernet_psk147 global _fernet_psk
141 if _fernet_psk is None:148 if _fernet_psk is None:
142 secret = get_shared_secret_from_filesystem()149 secret = get_shared_secret_from_filesystem()
143 assert secret is not None, "MAAS shared secret not found."150 if secret is None:
151 raise MissingSharedSecret("MAAS shared secret not found.")
144 # Keying material is required by PBKDF2 to be a byte string.152 # Keying material is required by PBKDF2 to be a byte string.
145 kdf = PBKDF2HMAC(153 kdf = PBKDF2HMAC(
146 algorithm=hashes.SHA256(),154 algorithm=hashes.SHA256(),
@@ -171,7 +179,7 @@
171 return f179 return f
172180
173181
174def fernet_encrypt(message):182def fernet_encrypt_psk(message, raw=False):
175 """Encrypts the specified message using the Fernet format.183 """Encrypts the specified message using the Fernet format.
176184
177 Returns the encrypted token, as a byte string.185 Returns the encrypted token, as a byte string.
@@ -183,29 +191,43 @@
183191
184 :param message: The message to encrypt.192 :param message: The message to encrypt.
185 :type message: Must be of type 'bytes' or a UTF-8 'str'.193 :type message: Must be of type 'bytes' or a UTF-8 'str'.
194 :param raw: if True, returns the decoded base64 bytes representing the
195 Fernet token. The bytes must be converted back to base64 to be
196 decrypted. (Or the 'raw' argument on the corresponding
197 fernet_decrypt_psk() function can be used.)
186 :return: the encryption token, as a base64-encoded byte string.198 :return: the encryption token, as a base64-encoded byte string.
187 """199 """
188 f = _get_fernet_context()200 fernet = _get_fernet_context()
189 if isinstance(message, str):201 if isinstance(message, str):
190 message = message.encode("utf-8")202 message = message.encode("utf-8")
191 return f.encrypt(message)203 token = fernet.encrypt(message)
192204 if raw is True:
193205 token = urlsafe_b64decode(token)
194def fernet_decrypt(token, ttl=None):206 return token
207
208
209def fernet_decrypt_psk(token, ttl=None, raw=False):
195 """Decrypts the specified Fernet token using the MAAS secret.210 """Decrypts the specified Fernet token using the MAAS secret.
196211
197 Returns the decrypted token as a byte string; the user is responsible for212 Returns the decrypted token as a byte string; the user is responsible for
198 converting it to the correct format or encoding.213 converting it to the correct format or encoding.
199214
200 :param message: The token to decrypt.215 :param message: The token to decrypt.
201 :type token: bytes216 :type token: Must be of type 'bytes', or an ASCII base64 string.
202 :param ttl: Optional amount of time (in seconds) allowed to have elapsed217 :param ttl: Optional amount of time (in seconds) allowed to have elapsed
203 before the message is rejected upon decryption. Note that the Fernet218 before the message is rejected upon decryption. Note that the Fernet
204 library considers times up to 60 seconds into the future (beyond the219 library considers times up to 60 seconds into the future (beyond the
205 TTL) to be valid.220 TTL) to be valid.
221 :param raw: if True, treats the string as the decoded base64 bytes of a
222 Fernet token, and attempts to encode them (as expected by the Fernet
223 APIs) before decrypting.
206 :return: bytes224 :return: bytes
207 """225 """
226 if raw is True:
227 token = urlsafe_b64encode(token)
208 f = _get_fernet_context()228 f = _get_fernet_context()
229 if isinstance(token, str):
230 token = token.encode("ascii")
209 return f.decrypt(token, ttl=ttl)231 return f.decrypt(token, ttl=ttl)
210232
211233
212234
=== modified file 'src/provisioningserver/tests/test_security.py'
--- src/provisioningserver/tests/test_security.py 2017-06-02 17:28:04 +0000
+++ src/provisioningserver/tests/test_security.py 2017-06-21 15:18:45 +0000
@@ -28,8 +28,9 @@
28from provisioningserver import security28from provisioningserver import security
29from provisioningserver.path import get_data_path29from provisioningserver.path import get_data_path
30from provisioningserver.security import (30from provisioningserver.security import (
31 fernet_decrypt,31 fernet_decrypt_psk,
32 fernet_encrypt,32 fernet_encrypt_psk,
33 MissingSharedSecret,
33)34)
34from provisioningserver.utils.fs import (35from provisioningserver.utils.fs import (
35 FileLock,36 FileLock,
@@ -37,7 +38,10 @@
37 write_text_file,38 write_text_file,
38)39)
39from testtools import ExpectedException40from testtools import ExpectedException
40from testtools.matchers import Equals41from testtools.matchers import (
42 Equals,
43 IsInstance,
44)
4145
4246
43class SharedSecretTestCase(MAASTestCase):47class SharedSecretTestCase(MAASTestCase):
@@ -49,17 +53,24 @@
49 # so that tests cannot interfere with each other.53 # so that tests cannot interfere with each other.
50 get_secret.return_value = get_data_path(54 get_secret.return_value = get_data_path(
51 "var", "lib", "maas", "secret-%s" % factory.make_string(16))55 "var", "lib", "maas", "secret-%s" % factory.make_string(16))
52 secret_file = security.get_shared_secret_filesystem_path()
53 # Extremely unlikely, but just in case.56 # Extremely unlikely, but just in case.
54 if os.path.isfile(secret_file):57 self.delete_secret()
55 os.remove(secret_file)58 self.addCleanup(
59 setattr, security, "DEFAULT_ITERATION_COUNT",
60 security.DEFAULT_ITERATION_COUNT)
61 # The default high iteration count would make the tests very slow.
62 security.DEFAULT_ITERATION_COUNT = 2
56 super().setUp()63 super().setUp()
5764
58 def tearDown(self):65 def tearDown(self):
66 self.delete_secret()
67 super().tearDown()
68
69 def delete_secret(self):
70 security._fernet_psk = None
59 secret_file = security.get_shared_secret_filesystem_path()71 secret_file = security.get_shared_secret_filesystem_path()
60 if os.path.isfile(secret_file):72 if os.path.isfile(secret_file):
61 os.remove(secret_file)73 os.remove(secret_file)
62 super().tearDown()
6374
64 def write_secret(self):75 def write_secret(self):
65 secret = factory.make_bytes()76 secret = factory.make_bytes()
@@ -121,6 +132,12 @@
121132
122class TestSetSharedSecretOnFilesystem(MAASTestCase):133class TestSetSharedSecretOnFilesystem(MAASTestCase):
123134
135 def test__default_iteration_count_is_reasonably_large(self):
136 # Ensure that the iteration count is high by default. This is very
137 # important so that the MAAS secret cannot be determined by
138 # brute-force.
139 self.assertThat(security.DEFAULT_ITERATION_COUNT, Equals(100000))
140
124 def read_secret(self):141 def read_secret(self):
125 secret_path = security.get_shared_secret_filesystem_path()142 secret_path = security.get_shared_secret_filesystem_path()
126 secret_hex = read_text_file(secret_path)143 secret_hex = read_text_file(secret_path)
@@ -299,40 +316,23 @@
299316
300class TestFernetEncryption(SharedSecretTestCase):317class TestFernetEncryption(SharedSecretTestCase):
301318
302 def setUp(self):
303 security._fernet_psk = None
304 # Ensure that the iteration count is high by default. This is very
305 # important so that the MAAS secret cannot be determined by
306 # brute-force. As a side effect, this ensures our tearDown (which
307 # resets the iteration count to its default) works properly.
308 self.assertThat(security.DEFAULT_ITERATION_COUNT, Equals(100000))
309 self._previous_iteration_count = security.DEFAULT_ITERATION_COUNT
310 # The default high iteration count would make the tests very slow.
311 security.DEFAULT_ITERATION_COUNT = 2
312 super().setUp()
313
314 def tearDown(self):
315 security._fernet_psk = None
316 security.DEFAULT_ITERATION_COUNT = self._previous_iteration_count
317 super().tearDown()
318
319 def test__first_encrypt_caches_psk(self):319 def test__first_encrypt_caches_psk(self):
320 self.write_secret()320 self.write_secret()
321 self.assertIsNone(security._fernet_psk)321 self.assertIsNone(security._fernet_psk)
322 testdata = factory.make_string()322 testdata = factory.make_string()
323 fernet_encrypt(testdata)323 fernet_encrypt_psk(testdata)
324 self.assertIsNotNone(security._fernet_psk)324 self.assertIsNotNone(security._fernet_psk)
325325
326 def test__derives_identical_key_on_decrypt(self):326 def test__derives_identical_key_on_decrypt(self):
327 self.write_secret()327 self.write_secret()
328 self.assertIsNone(security._fernet_psk)328 self.assertIsNone(security._fernet_psk)
329 testdata = factory.make_bytes()329 testdata = factory.make_bytes()
330 token = fernet_encrypt(testdata)330 token = fernet_encrypt_psk(testdata)
331 first_key = security._fernet_psk331 first_key = security._fernet_psk
332 # Make it seem like we're decrypting something without ever encrypting332 # Make it seem like we're decrypting something without ever encrypting
333 # anything first.333 # anything first.
334 security._fernet_psk = None334 security._fernet_psk = None
335 decrypted = fernet_decrypt(token)335 decrypted = fernet_decrypt_psk(token)
336 second_key = security._fernet_psk336 second_key = security._fernet_psk
337 self.assertEqual(first_key, second_key)337 self.assertEqual(first_key, second_key)
338 self.assertEqual(testdata, decrypted)338 self.assertEqual(testdata, decrypted)
@@ -340,29 +340,40 @@
340 def test__can_encrypt_and_decrypt_string(self):340 def test__can_encrypt_and_decrypt_string(self):
341 self.write_secret()341 self.write_secret()
342 testdata = factory.make_string()342 testdata = factory.make_string()
343 token = fernet_encrypt(testdata)343 token = fernet_encrypt_psk(testdata)
344 decrypted = fernet_decrypt(token)344 # Round-trip this to a string, since Fernet tokens are used inside
345 decrypted = decrypted.decode("utf-8")345 # strings (such as JSON objects) typically.
346 token = token.decode("ascii")
347 decrypted = fernet_decrypt_psk(token)
348 decrypted = decrypted.decode("ascii")
349 self.assertThat(decrypted, Equals(testdata))
350
351 def test__can_encrypt_and_decrypt_with_raw_bytes(self):
352 self.write_secret()
353 testdata = factory.make_bytes()
354 token = fernet_encrypt_psk(testdata, raw=True)
355 self.assertThat(token, IsInstance(bytes))
356 decrypted = fernet_decrypt_psk(token, raw=True)
346 self.assertThat(decrypted, Equals(testdata))357 self.assertThat(decrypted, Equals(testdata))
347358
348 def test__can_encrypt_and_decrypt_bytes(self):359 def test__can_encrypt_and_decrypt_bytes(self):
349 self.write_secret()360 self.write_secret()
350 testdata = factory.make_bytes()361 testdata = factory.make_bytes()
351 token = fernet_encrypt(testdata)362 token = fernet_encrypt_psk(testdata)
352 decrypted = fernet_decrypt(token)363 decrypted = fernet_decrypt_psk(token)
353 self.assertThat(decrypted, Equals(testdata))364 self.assertThat(decrypted, Equals(testdata))
354365
355 def test__raises_when_no_secret_exists(self):366 def test__raises_when_no_secret_exists(self):
356 testdata = factory.make_bytes()367 testdata = factory.make_bytes()
357 with ExpectedException(AssertionError):368 with ExpectedException(MissingSharedSecret):
358 fernet_encrypt(testdata)369 fernet_encrypt_psk(testdata)
359 with ExpectedException(AssertionError):370 with ExpectedException(MissingSharedSecret):
360 fernet_decrypt(b"")371 fernet_decrypt_psk(b"")
361372
362 def test__assures_data_integrity(self):373 def test__assures_data_integrity(self):
363 self.write_secret()374 self.write_secret()
364 testdata = factory.make_bytes(size=10)375 testdata = factory.make_bytes(size=10)
365 token = fernet_encrypt(testdata)376 token = fernet_encrypt_psk(testdata)
366 bad_token = bytearray(token)377 bad_token = bytearray(token)
367 # Flip a bit in the token, so we can ensure it won't decrypt if it378 # Flip a bit in the token, so we can ensure it won't decrypt if it
368 # has been corrupted. Subtract 4 to avoid the end of the token; that379 # has been corrupted. Subtract 4 to avoid the end of the token; that
@@ -374,30 +385,30 @@
374 test_description = ("token=%s; token[%d] ^= 0x%02x" % (385 test_description = ("token=%s; token[%d] ^= 0x%02x" % (
375 token.decode("utf-8"), byte_to_flip, bit_to_flip))386 token.decode("utf-8"), byte_to_flip, bit_to_flip))
376 with ExpectedException(InvalidToken, msg=test_description):387 with ExpectedException(InvalidToken, msg=test_description):
377 fernet_decrypt(bad_token)388 fernet_decrypt_psk(bad_token)
378389
379 def test__messages_from_up_to_a_minute_in_the_future_accepted(self):390 def test__messages_from_up_to_a_minute_in_the_future_accepted(self):
380 self.write_secret()391 self.write_secret()
381 testdata = factory.make_bytes()392 testdata = factory.make_bytes()
382 now = time.time()393 now = time.time()
383 self.patch(time, "time").side_effect = [now + 60, now]394 self.patch(time, "time").side_effect = [now + 60, now]
384 token = fernet_encrypt(testdata)395 token = fernet_encrypt_psk(testdata)
385 fernet_decrypt(token, ttl=1)396 fernet_decrypt_psk(token, ttl=1)
386397
387 def test__messages_from_the_past_exceeding_ttl_rejected(self):398 def test__messages_from_the_past_exceeding_ttl_rejected(self):
388 self.write_secret()399 self.write_secret()
389 testdata = factory.make_bytes()400 testdata = factory.make_bytes()
390 now = time.time()401 now = time.time()
391 self.patch(time, "time").side_effect = [now - 2, now]402 self.patch(time, "time").side_effect = [now - 2, now]
392 token = fernet_encrypt(testdata)403 token = fernet_encrypt_psk(testdata)
393 with ExpectedException(InvalidToken):404 with ExpectedException(InvalidToken):
394 fernet_decrypt(token, ttl=1)405 fernet_decrypt_psk(token, ttl=1)
395406
396 def test__messages_from_future_exceeding_clock_skew_limit_rejected(self):407 def test__messages_from_future_exceeding_clock_skew_limit_rejected(self):
397 self.write_secret()408 self.write_secret()
398 testdata = factory.make_bytes()409 testdata = factory.make_bytes()
399 now = time.time()410 now = time.time()
400 self.patch(time, "time").side_effect = [now + 61, now]411 self.patch(time, "time").side_effect = [now + 61, now]
401 token = fernet_encrypt(testdata)412 token = fernet_encrypt_psk(testdata)
402 with ExpectedException(InvalidToken):413 with ExpectedException(InvalidToken):
403 fernet_decrypt(token, ttl=1)414 fernet_decrypt_psk(token, ttl=1)
404415
=== modified file 'src/provisioningserver/utils/beaconing.py'
--- src/provisioningserver/utils/beaconing.py 2017-06-13 05:26:53 +0000
+++ src/provisioningserver/utils/beaconing.py 2017-06-21 15:18:45 +0000
@@ -5,18 +5,35 @@
55
6__all__ = [6__all__ = [
7 "BeaconingPacket",7 "BeaconingPacket",
8 "BeaconPayload",
9 "InvalidBeaconingPacket",
10 "create_beacon_payload",
11 "read_beacon_payload",
8 "add_arguments",12 "add_arguments",
9 "run"13 "run"
10]14]
1115
16from collections import namedtuple
17from gzip import (
18 compress,
19 decompress,
20)
12import json21import json
13import os22import os
14import stat23import stat
24import struct
15import subprocess25import subprocess
16import sys26import sys
17from textwrap import dedent27from textwrap import dedent
28import uuid
1829
19import bson30from bson import BSON
31from bson.errors import BSONError
32from cryptography.fernet import InvalidToken
33from provisioningserver.security import (
34 fernet_decrypt_psk,
35 fernet_encrypt_psk,
36)
20from provisioningserver.utils import sudo37from provisioningserver.utils import sudo
21from provisioningserver.utils.network import format_eui38from provisioningserver.utils.network import format_eui
22from provisioningserver.utils.pcap import (39from provisioningserver.utils.pcap import (
@@ -30,8 +47,119 @@
30)47)
3148
3249
50BEACON_PORT = 5240
51
52BEACON_TYPES = {
53 "solicitation": 1,
54 "advertisement": 2
55}
56
57BEACON_TYPE_VALUES = {
58 value: name for name, value in BEACON_TYPES.items()
59}
60
61PROTOCOL_VERSION = 1
62BEACON_HEADER_FORMAT_V1 = "!BBH"
63BEACON_HEADER_LENGTH_V1 = 4
64
65
66BeaconPayload = namedtuple('BeaconPayload', (
67 'bytes',
68 'version',
69 'type',
70 'payload',
71))
72
73
74def create_beacon_payload(beacon_type, payload=None, version=PROTOCOL_VERSION):
75 """Creates a beacon payload of the specified type, with the given data.
76
77 :param beacon_type: The beacon packet type. Indicates the purpose of the
78 beacon to the receiver.
79 :param payload: Optional JSON-encodable dictionary. Will be converted to an
80 inner encrypted payload and presented in the "data" field in the
81 resulting dictionary.
82 :param version: Optional protocol version to use (defaults to most recent).
83 :return: BeaconPayload namedtuple representing the packet bytes, the outer
84 payload, and the inner encrypted data (if any).
85 """
86 beacon_type_code = BEACON_TYPES[beacon_type]
87 if payload is not None:
88 payload = payload.copy()
89 payload["uuid"] = str(uuid.uuid1())
90 payload["type"] = beacon_type_code
91 data_bytes = BSON.encode(payload)
92 compressed_bytes = compress(data_bytes, compresslevel=9)
93 payload_bytes = fernet_encrypt_psk(compressed_bytes, raw=True)
94 else:
95 payload_bytes = b''
96 beacon_bytes = struct.pack(
97 BEACON_HEADER_FORMAT_V1 + "%ds" % len(payload_bytes),
98 version, beacon_type_code, len(payload_bytes), payload_bytes)
99 return BeaconPayload(
100 beacon_bytes, version, BEACON_TYPE_VALUES[beacon_type_code], payload)
101
102
103def read_beacon_payload(beacon_bytes):
104 """Returns a BeaconPayload namedtuple representing the given beacon bytes.
105
106 Decrypts the inner beacon data if necessary.
107
108 :param beacon_bytes: beacon payload (bytes).
109 :return: dict
110 """
111 if len(beacon_bytes) < BEACON_HEADER_LENGTH_V1:
112 raise InvalidBeaconingPacket(
113 "Beaconing packet must be at least %d bytes." % (
114 BEACON_HEADER_LENGTH_V1))
115 header = beacon_bytes[:BEACON_HEADER_LENGTH_V1]
116 version, beacon_type_code, expected_payload_length = struct.unpack(
117 BEACON_HEADER_FORMAT_V1, header)
118 actual_payload_length = len(beacon_bytes) - BEACON_HEADER_LENGTH_V1
119 if len(beacon_bytes) - BEACON_HEADER_LENGTH_V1 < expected_payload_length:
120 raise InvalidBeaconingPacket(
121 "Invalid payload length: expected %d bytes, got %d bytes." % (
122 expected_payload_length, actual_payload_length))
123 payload_start = BEACON_HEADER_LENGTH_V1
124 payload_end = BEACON_HEADER_LENGTH_V1 + expected_payload_length
125 payload_bytes = beacon_bytes[payload_start:payload_end]
126 payload = None
127 if version == 1:
128 if len(payload_bytes) == 0:
129 # No encrypted inner payload; nothing to do.
130 pass
131 else:
132 try:
133 decrypted_data = fernet_decrypt_psk(
134 payload_bytes, raw=True)
135 except InvalidToken:
136 raise InvalidBeaconingPacket(
137 "Failed to decrypt inner payload: check MAAS secret key.")
138 try:
139 decompressed_data = decompress(decrypted_data)
140 except OSError:
141 raise InvalidBeaconingPacket(
142 "Failed to decompress inner payload: %r" % decrypted_data)
143 try:
144 # Replace the data in the dictionary with its decrypted form.
145 payload = BSON.decode(decompressed_data)
146 except BSONError:
147 raise InvalidBeaconingPacket(
148 "Inner beacon payload is not BSON: %r" % decompressed_data)
149 else:
150 raise InvalidBeaconingPacket(
151 "Unknown beacon version: %d" % version)
152 beacon_type_code = payload["type"] if payload else beacon_type_code
153 return BeaconPayload(
154 beacon_bytes, version, BEACON_TYPE_VALUES[beacon_type_code], payload)
155
156
33class InvalidBeaconingPacket(Exception):157class InvalidBeaconingPacket(Exception):
34 """Raised internally when a beaconing packet is not valid."""158 """Raised when a beaconing packet is not valid."""
159
160 def __init__(self, invalid_reason):
161 self.invalid_reason = invalid_reason
162 super().__init__(invalid_reason)
35163
36164
37class BeaconingPacket:165class BeaconingPacket:
@@ -57,12 +185,12 @@
57 :param out: An object with `write(str)` and `flush()` methods.185 :param out: An object with `write(str)` and `flush()` methods.
58 """186 """
59 try:187 try:
60 payload = bson.decode_all(self.packet)188 payload = read_beacon_payload(self.packet)
61 self.valid = True189 self.valid = True
62 return payload190 return payload
63 except bson.InvalidBSON:191 except InvalidBeaconingPacket as ibp:
64 self.valid = False192 self.valid = False
65 self.invalid_reason = "Packet payload is not BSON."193 self.invalid_reason = ibp.invalid_reason
66 return None194 return None
67195
68196
@@ -90,6 +218,8 @@
90 "destination_mac": format_eui(packet.l2.dst_eui),218 "destination_mac": format_eui(packet.l2.dst_eui),
91 "source_ip": str(packet.l3.src_ip),219 "source_ip": str(packet.l3.src_ip),
92 "destination_ip": str(packet.l3.dst_ip),220 "destination_ip": str(packet.l3.dst_ip),
221 "source_port": packet.l4.packet.src_port,
222 "destination_port": packet.l4.packet.dst_port,
93 }223 }
94 if packet.l2.vid is not None:224 if packet.l2.vid is not None:
95 output_json["vid"] = packet.l2.vid225 output_json["vid"] = packet.l2.vid
@@ -99,11 +229,6 @@
99 out.write(json.dumps(output_json))229 out.write(json.dumps(output_json))
100 out.write('\n')230 out.write('\n')
101 out.flush()231 out.flush()
102 else:
103 err.write(
104 "Invalid beacon payload (not BSON): %r.\n" % (
105 beacon.packet))
106 err.flush()
107 except PacketProcessingError as e:232 except PacketProcessingError as e:
108 err.write(e.error)233 err.write(e.error)
109 err.write("\n")234 err.write("\n")
110235
=== modified file 'src/provisioningserver/utils/tests/test_beaconing.py'
--- src/provisioningserver/utils/tests/test_beaconing.py 2017-06-13 16:09:05 +0000
+++ src/provisioningserver/utils/tests/test_beaconing.py 2017-06-21 15:18:45 +0000
@@ -6,39 +6,152 @@
6__all__ = []6__all__ = []
77
8from argparse import ArgumentParser8from argparse import ArgumentParser
9from gzip import compress
9import io10import io
11import random
12import struct
10import subprocess13import subprocess
11from tempfile import NamedTemporaryFile14from tempfile import NamedTemporaryFile
12from unittest.mock import Mock15from unittest.mock import Mock
16from uuid import UUID
1317
14from bson import BSON
15from maastesting.factory import factory18from maastesting.factory import factory
16from maastesting.matchers import MockCalledOnceWith19from maastesting.matchers import MockCalledOnceWith
17from maastesting.testcase import MAASTestCase20from maastesting.testcase import MAASTestCase
21from provisioningserver.security import (
22 fernet_encrypt_psk,
23 MissingSharedSecret,
24)
25from provisioningserver.tests.test_security import SharedSecretTestCase
18from provisioningserver.utils import beaconing as beaconing_module26from provisioningserver.utils import beaconing as beaconing_module
19from provisioningserver.utils.beaconing import (27from provisioningserver.utils.beaconing import (
20 add_arguments,28 add_arguments,
29 BEACON_HEADER_FORMAT_V1,
30 BEACON_TYPES,
21 BeaconingPacket,31 BeaconingPacket,
32 create_beacon_payload,
33 InvalidBeaconingPacket,
34 read_beacon_payload,
22 run,35 run,
23)36)
24from provisioningserver.utils.script import ActionScriptError37from provisioningserver.utils.script import ActionScriptError
38from testtools.matchers import (
39 Equals,
40 Is,
41 IsInstance,
42)
25from testtools.testcase import ExpectedException43from testtools.testcase import ExpectedException
2644
2745
28def make_beaconing_packet(payload):46class TestCreateBeaconPayload(SharedSecretTestCase):
29 # Beaconing packets are BSON-encoded byte strings.47
30 beaconing_packet = BSON.encode(payload)48 def test__requires_maas_shared_secret_for_inner_data_payload(self):
31 return beaconing_packet49 with ExpectedException(
50 MissingSharedSecret, ".*shared secret not found.*"):
51 create_beacon_payload("solicitation", payload={})
52
53 def test__returns_beaconpayload_namedtuple(self):
54 beacon = create_beacon_payload("solicitation")
55 self.assertThat(beacon.bytes, IsInstance(bytes))
56 self.assertThat(beacon.payload, Is(None))
57 self.assertThat(beacon.type, Equals("solicitation"))
58 self.assertThat(beacon.version, Equals(1))
59
60 def test__succeeds_when_shared_secret_present(self):
61 self.write_secret()
62 beacon = create_beacon_payload(
63 "solicitation", payload={})
64 self.assertThat(beacon.type, Equals("solicitation"))
65 self.assertThat(
66 beacon.payload['type'], Equals(BEACON_TYPES["solicitation"]))
67
68 def test__supplements_data_and_returns_complete_data(self):
69 self.write_secret()
70 random_type = random.choice(list(BEACON_TYPES.keys()))
71 random_key = factory.make_string(prefix="_")
72 random_value = factory.make_string()
73 beacon = create_beacon_payload(
74 random_type, payload={random_key: random_value})
75 # Ensure a valid UUID was added.
76 self.assertIsNotNone(UUID(beacon.payload['uuid']))
77 self.assertThat(beacon.type, Equals(random_type))
78 # The type is replicated here for authentication purposes.
79 self.assertThat(
80 beacon.payload['type'], Equals(BEACON_TYPES[random_type]))
81 self.assertThat(beacon.payload[random_key], Equals(random_value))
82
83 def test__creates_packet_that_can_decode(self):
84 self.write_secret()
85 random_type = random.choice(list(BEACON_TYPES.keys()))
86 random_key = factory.make_string(prefix="_")
87 random_value = factory.make_string()
88 packet_bytes, _, _, _ = create_beacon_payload(
89 random_type, payload={random_key: random_value})
90 decrypted = read_beacon_payload(packet_bytes)
91 self.assertThat(decrypted.type, Equals(random_type))
92 self.assertThat(decrypted.payload[random_key], Equals(random_value))
93
94
95def _make_beacon_payload(version=1, type_code=1, length=None, payload=None):
96 if payload is None:
97 payload = b''
98 if length is None:
99 length = len(payload)
100 packet = struct.pack(BEACON_HEADER_FORMAT_V1, version, type_code, length)
101 return packet + payload
102
103
104class TestReadBeaconPayload(SharedSecretTestCase):
105
106 def test__raises_if_packet_too_small(self):
107 with ExpectedException(
108 InvalidBeaconingPacket, ".*packet must be at least 4 bytes.*"):
109 read_beacon_payload(b"")
110
111 def test__raises_if_payload_too_small(self):
112 packet = _make_beacon_payload(payload=b'1234')[:6]
113 with ExpectedException(
114 InvalidBeaconingPacket, ".*expected 4 bytes, got 2 bytes.*"):
115 read_beacon_payload(packet)
116
117 def test__raises_when_version_incorrect(self):
118 packet = _make_beacon_payload(version=0xfe)
119 with ExpectedException(
120 InvalidBeaconingPacket, ".*Unknown beacon version.*"):
121 read_beacon_payload(packet)
122
123 def test__raises_when_inner_payload_does_not_decrypt(self):
124 self.write_secret()
125 packet = _make_beacon_payload(payload=b'\xfe')
126 with ExpectedException(
127 InvalidBeaconingPacket, ".*Failed to decrypt.*"):
128 read_beacon_payload(packet)
129
130 def test__raises_when_inner_encapsulation_does_not_decompress(self):
131 self.write_secret()
132 packet = _make_beacon_payload(
133 payload=fernet_encrypt_psk('\n\n', raw=True))
134 with ExpectedException(
135 InvalidBeaconingPacket, ".*Failed to decompress.*"):
136 read_beacon_payload(packet)
137
138 def test__raises_when_inner_encapsulation_is_not_bson(self):
139 self.write_secret()
140 payload = fernet_encrypt_psk(compress(b"\n\n"), raw=True)
141 packet = _make_beacon_payload(payload=payload)
142 with ExpectedException(
143 InvalidBeaconingPacket, ".*beacon payload is not BSON.*"):
144 read_beacon_payload(packet)
32145
33146
34class TestBeaconingPacket(MAASTestCase):147class TestBeaconingPacket(MAASTestCase):
35148
36 def test__is_valid__succeeds_for_valid_bson(self):149 def test__is_valid__succeeds_for_valid_payload(self):
37 packet = make_beaconing_packet({"testing": 123})150 beacon = create_beacon_payload("solicitation")
38 beacon = BeaconingPacket(packet)151 beacon_packet = BeaconingPacket(beacon.bytes)
39 self.assertTrue(beacon.valid)152 self.assertTrue(beacon_packet.valid)
40153
41 def test__is_valid__fails_for_invalid_bson(self):154 def test__is_valid__fails_for_invalid_payload(self):
42 beacon = BeaconingPacket(b"\n\n\n\n")155 beacon = BeaconingPacket(b"\n\n\n\n")
43 self.assertFalse(beacon.valid)156 self.assertFalse(beacon.valid)
44157
@@ -48,7 +161,7 @@
48 b'\x00@\x00\x00\x01\x00\x00\x00v\xe19Y\xadF\x08\x00^\x00\x00\x00^\x00\x00'161 b'\x00@\x00\x00\x01\x00\x00\x00v\xe19Y\xadF\x08\x00^\x00\x00\x00^\x00\x00'
49 b'\x00\x01\x00^\x00\x00v\x00\x16>\x91zz\x08\x00E\x00\x00P\xe2E@\x00\x01'162 b'\x00\x01\x00^\x00\x00v\x00\x16>\x91zz\x08\x00E\x00\x00P\xe2E@\x00\x01'
50 b'\x11\xe0\xce\xac\x10*\x02\xe0\x00\x00v\xda\xc2\x14x\x00<h(4\x00\x00\x00'163 b'\x11\xe0\xce\xac\x10*\x02\xe0\x00\x00v\xda\xc2\x14x\x00<h(4\x00\x00\x00'
51 b'\x02uuid\x00%\x00\x00\x0078d1a4f0-4ca4-11e7-b2bb-00163e917a7a\x00\x00')164 b'\x02uuid\x00%\x00\x00\x0000000000-0000-0000-0000-000000000000\x00\x00')
52165
53166
54class TestObserveBeaconsCommand(MAASTestCase):167class TestObserveBeaconsCommand(MAASTestCase):