mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 18:51:16 +00:00
Support atomic two-packet signed fleet commands
Automatically split longer signed envelopes into two bounded group-data packets, reserve both before queueing, and roll back exact packet copies and retries on admission failure. Collect fragments in either order with a fixed deadline and verify the complete command before durable replay reservation and dispatch. Raise whole-fleet protocol capacity to 230 bytes, preserve target-specific budgets and radio acknowledgement barriers, and reject rule-token and USB rescue-line truncation. Document existing app input limits and add real-signature, sanitizer, queue failure, timeout, replay, and compact-board regressions.
This commit is contained in:
1 parent
d74a0f8b44
commit
fdad79eaf7
18 files changed
+1649
-143
No files matched your search
@@ -34,6 +34,8 @@ FIXTURE = r'''
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
inline unsigned fleet_sign_calls = 0;
|
||||
|
||||
// The production Identity interface is retained. OpenSSL implements its
|
||||
// signatures here so these tests cannot inherit native's always-true mock.
|
||||
namespace mesh {
|
||||
@@ -46,6 +48,7 @@ LocalIdentity::LocalIdentity(RNG* rng) {
|
||||
EVP_PKEY_free(key);
|
||||
}
|
||||
void LocalIdentity::sign(uint8_t* signature, const uint8_t* message, int length) const {
|
||||
++fleet_sign_calls;
|
||||
EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, prv_key, 32);
|
||||
EVP_MD_CTX* context = EVP_MD_CTX_new(); assert(key && context);
|
||||
assert(EVP_DigestSignInit(context, nullptr, nullptr, nullptr, key) == 1);
|
||||
@@ -84,26 +87,83 @@ void hex(const uint8_t* bytes, size_t size) {
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
using Fleet = mesh::FleetCommand;
|
||||
if (argc == 7 && std::string(argv[1]) == "encode") {
|
||||
const std::string operation = argc > 1 ? argv[1] : "";
|
||||
if (argc == 7 && (operation == "encode" || operation == "encode-wide")) {
|
||||
FixedRng rng; mesh::LocalIdentity publisher(&rng);
|
||||
const auto key = unhex(argv[2]); assert(key.size() == Fleet::KeySize);
|
||||
std::array<uint8_t, Fleet::TargetSize> target;
|
||||
if (!Fleet::parseTarget(argv[3], target.data())) { std::cout << "reject\n"; return 0; }
|
||||
std::array<uint8_t, Fleet::MaxPayloadLength> output;
|
||||
std::array<uint8_t, Fleet::MaxEnvelopeLength + 64> output;
|
||||
output.fill(0xA5);
|
||||
const unsigned signed_before = fleet_sign_calls;
|
||||
const size_t capacity = operation == "encode-wide" ? output.size() : Fleet::MaxEnvelopeLength;
|
||||
const size_t size = Fleet::encode(publisher, key.data(), uint32_t(std::stoul(argv[4])),
|
||||
uint32_t(std::stoul(argv[5])), target.data(), argv[6], output.data(), output.size());
|
||||
if (!size) std::cout << "reject\n"; else hex(output.data(), size);
|
||||
uint32_t(std::stoul(argv[5])), target.data(), argv[6], output.data(), capacity);
|
||||
assert(size <= Fleet::MaxEnvelopeLength);
|
||||
if (!size) {
|
||||
assert(fleet_sign_calls == signed_before);
|
||||
for (uint8_t byte : output) assert(byte == 0xA5);
|
||||
std::cout << "reject\n";
|
||||
} else {
|
||||
assert(fleet_sign_calls == signed_before + 1);
|
||||
for (size_t i = size; i < output.size(); ++i) assert(output[i] == 0xA5);
|
||||
hex(output.data(), size);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (argc == 7 && std::string(argv[1]) == "encode2") {
|
||||
if (argc == 7 && (operation == "encode2" || operation == "encode2-wide")) {
|
||||
FixedRng rng; mesh::LocalIdentity publisher(&rng);
|
||||
const auto key = unhex(argv[2]); assert(key.size() == Fleet::KeySize);
|
||||
Fleet::Targets targets;
|
||||
if (!Fleet::parseTargets(argv[3], strlen(argv[3]), targets)) { std::cout << "reject\n"; return 0; }
|
||||
std::array<uint8_t, Fleet::MaxPayloadLength> output;
|
||||
std::array<uint8_t, Fleet::MaxEnvelopeLength + 64> output;
|
||||
output.fill(0xA5);
|
||||
const unsigned signed_before = fleet_sign_calls;
|
||||
const size_t capacity = operation == "encode2-wide" ? output.size() : Fleet::MaxEnvelopeLength;
|
||||
const size_t size = Fleet::encode(publisher, key.data(), uint32_t(std::stoul(argv[4])),
|
||||
uint32_t(std::stoul(argv[5])), targets, argv[6], output.data(), output.size());
|
||||
if (!size) std::cout << "reject\n"; else hex(output.data(), size);
|
||||
uint32_t(std::stoul(argv[5])), targets, argv[6], output.data(), capacity);
|
||||
assert(size <= Fleet::MaxEnvelopeLength);
|
||||
if (!size) {
|
||||
assert(fleet_sign_calls == signed_before);
|
||||
for (uint8_t byte : output) assert(byte == 0xA5);
|
||||
std::cout << "reject\n";
|
||||
} else {
|
||||
assert(fleet_sign_calls == signed_before + 1);
|
||||
for (size_t i = size; i < output.size(); ++i) assert(output[i] == 0xA5);
|
||||
hex(output.data(), size);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (argc == 5 && operation == "fragment") {
|
||||
const auto envelope = unhex(argv[2]);
|
||||
const unsigned index = unsigned(std::stoul(argv[3]));
|
||||
assert(index <= 255);
|
||||
const size_t capacity = std::stoul(argv[4]);
|
||||
std::array<uint8_t, Fleet::MaxPayloadLength + 64> output;
|
||||
assert(capacity <= output.size()); output.fill(0xA5);
|
||||
const unsigned signed_before = fleet_sign_calls;
|
||||
const size_t size = Fleet::fragment(envelope.data(), envelope.size(), uint8_t(index),
|
||||
output.data(), capacity);
|
||||
assert(fleet_sign_calls == signed_before && size <= Fleet::MaxPayloadLength);
|
||||
if (!size) {
|
||||
for (uint8_t byte : output) assert(byte == 0xA5);
|
||||
std::cout << "reject\n";
|
||||
} else {
|
||||
for (size_t i = size; i < output.size(); ++i) assert(output[i] == 0xA5);
|
||||
hex(output.data(), size);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (argc == 3 && operation == "parse-fragment") {
|
||||
const auto payload = unhex(argv[2]);
|
||||
Fleet::Fragment output{123, 300, 1, payload.data(), 100};
|
||||
if (!Fleet::parseFragment(payload.data(), payload.size(), output)) {
|
||||
assert(!output.sequence && !output.total_length && !output.index && !output.data && !output.length);
|
||||
std::cout << "reject\n";
|
||||
} else {
|
||||
std::cout << output.sequence << '\n' << output.total_length << '\n' << unsigned(output.index) << '\n';
|
||||
hex(output.data, output.length);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (argc == 7 && (std::string(argv[1]) == "decode" || std::string(argv[1]) == "decode2")) {
|
||||
@@ -142,6 +202,13 @@ def envelope2(entries=(), command="get radio2", sequence=NOW, expires=NOW + 120,
|
||||
return unsigned + publisher.sign(DOMAIN + channel + unsigned)
|
||||
|
||||
|
||||
def fragment_frame(payload, index):
|
||||
"""Independent wire framing; the signature stays in the whole envelope."""
|
||||
sequence = struct.unpack("<I", payload[4:8])[0]
|
||||
return (b"FMP1" + struct.pack("<IHB", sequence, len(payload), index)
|
||||
+ payload[index * 154:(index + 1) * 154])
|
||||
|
||||
|
||||
class FleetCommandCryptoTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -299,8 +366,18 @@ class FleetCommandCryptoTests(unittest.TestCase):
|
||||
self.assertEqual(len(maximum), 72)
|
||||
self.assertNotEqual(self.decode(envelope(maximum)), "reject")
|
||||
self.assertNotEqual(self.decode(envelope2(entries, maximum)), "reject")
|
||||
oversized = prefix + "0" + maximum[len(prefix):]
|
||||
self.assertEqual(self.decode(envelope(oversized)), "reject")
|
||||
larger = prefix + "0" + maximum[len(prefix):]
|
||||
self.assertNotEqual(self.decode(envelope(larger)), "reject")
|
||||
self.assertNotEqual(self.decode(envelope2(entries, larger)), "reject")
|
||||
target_maximum = prefix + "0" * (218 - len(prefix) - len(suffix)) + suffix
|
||||
self.assertNotEqual(self.decode(envelope2(entries, target_maximum)), "reject")
|
||||
too_large = prefix + "0" + target_maximum[len(prefix):]
|
||||
self.assertEqual(self.decode(envelope2(entries, too_large)), "reject")
|
||||
broadcast_maximum = prefix + "0" * (230 - len(prefix) - len(suffix)) + suffix
|
||||
expected = envelope2(command=broadcast_maximum)
|
||||
self.assertEqual(self.run_tool("encode2", CHANNEL.hex(), "all", NOW, NOW + 120,
|
||||
broadcast_maximum), expected.hex())
|
||||
self.assertNotEqual(self.decode(expected), "reject")
|
||||
|
||||
def test_clock_management_has_real_signatures_and_no_prefix_aliases(self):
|
||||
for command in ["clock", "clock sync", "time 1735689600", "time 1800000000", "time 4294967295"]:
|
||||
@@ -333,12 +410,18 @@ class FleetCommandCryptoTests(unittest.TestCase):
|
||||
self.assertEqual(self.decode(forged), "reject")
|
||||
self.assertNotEqual(self.decode(original, now=NOW + 120), "reject")
|
||||
|
||||
def test_exact_and_oversized_command_boundaries(self):
|
||||
maximum = "set flood.rule.1 " + "a" * (72 - len("set flood.rule.1 "))
|
||||
def test_fixed_target_exact_limit_cannot_be_raised_by_destination_capacity(self):
|
||||
maximum = "set flood.rule.1 " + "a" * (215 - len("set flood.rule.1 "))
|
||||
accepted = envelope(maximum)
|
||||
self.assertEqual(len(accepted), 165)
|
||||
self.assertEqual(len(accepted), 308)
|
||||
self.assertNotEqual(self.decode(accepted), "reject")
|
||||
self.assertEqual(self.decode(envelope(maximum + "a")), "reject")
|
||||
for operation in ["encode", "encode-wide"]:
|
||||
self.assertEqual(self.run_tool(operation, CHANNEL.hex(), "all", NOW, NOW + 120, maximum),
|
||||
accepted.hex())
|
||||
for length in [216, 230, 231]:
|
||||
command = "set flood.rule.1 " + "a" * (length - len("set flood.rule.1 "))
|
||||
self.assertEqual(self.run_tool(operation, CHANNEL.hex(), "all", NOW, NOW + 120, command), "reject")
|
||||
|
||||
def test_valid_signature_does_not_override_clock_policy(self):
|
||||
for sequence, expires, now in [
|
||||
@@ -412,23 +495,193 @@ class FleetCommandCryptoTests(unittest.TestCase):
|
||||
self.assertEqual(self.decode(original, publisher=OTHER_PUBLIC), "reject")
|
||||
self.assertEqual(self.decode(original, channel=bytes(range(17, 33))), "reject")
|
||||
|
||||
def test_compact_broadcast_keeps_72_byte_command_and_packet_framing_limits(self):
|
||||
def test_compact_broadcast_fits_230_byte_signed_command_and_framing_limits(self):
|
||||
prefix = "set flood.rule.1 "
|
||||
maximum = prefix + "a" * (72 - len(prefix))
|
||||
maximum = prefix + "a" * (230 - len(prefix))
|
||||
expected = envelope2(command=maximum)
|
||||
actual = self.run_tool("encode2", CHANNEL.hex(), "all", NOW, NOW + 120, maximum)
|
||||
self.assertEqual(actual, expected.hex())
|
||||
self.assertEqual(len(expected), 150)
|
||||
self.assertEqual(len(envelope(maximum)) - len(expected), 15)
|
||||
self.assertNotEqual(self.decode(expected), "reject")
|
||||
self.assertEqual(len(expected), 308)
|
||||
PUBLISHER.public_key().verify(expected[-64:], DOMAIN + CHANNEL + expected[:-64])
|
||||
for node in [PUBLIC, OTHER_PUBLIC]:
|
||||
self.assertEqual(self.decode(expected, self_key=node, metadata=True),
|
||||
f"{NOW}\n{NOW + 120}\n{maximum}\n1")
|
||||
oversized = maximum + "a"
|
||||
self.assertEqual(self.decode(envelope2(command=oversized)), "reject")
|
||||
self.assertEqual(self.run_tool("encode2", CHANNEL.hex(), "all", NOW, NOW + 120, oversized), "reject")
|
||||
self.assertEqual(self.run_tool("encode2-wide", CHANNEL.hex(), "all", NOW, NOW + 120, oversized), "reject")
|
||||
for invalid in [b"\n", b";", b"\x80", b"\0"]:
|
||||
malformed = maximum[:-1].encode("ascii") + invalid
|
||||
self.assertEqual(self.decode(envelope2(command=malformed)), "reject")
|
||||
for length in [0, 12, 13, 14, len(expected) - 64, len(expected) - 1]:
|
||||
with self.subTest(length=length):
|
||||
self.assertEqual(self.decode(expected[:length]), "reject")
|
||||
self.assertEqual(self.decode(expected + b"\0"), "reject")
|
||||
|
||||
def test_each_target_form_has_exact_packet_budget_and_atomic_oversize_rejection(self):
|
||||
short, long = PUBLIC[:4].hex(), PUBLIC[:6].hex()
|
||||
digest = hashlib.sha256(PUBLIC).digest()[:16]
|
||||
for token, entries, maximum_length in [
|
||||
("all", [], 230), (short, [(4, PUBLIC[:4])], 225),
|
||||
(long, [(6, PUBLIC[:6])], 223), (PUBLIC.hex(), None, 215),
|
||||
(f"{short},{short}", [(4, PUBLIC[:4])] * 2, 220),
|
||||
(f"{short},{long}", [(4, PUBLIC[:4]), (6, PUBLIC[:6])], 218),
|
||||
(f"{PUBLIC.hex()},{short}", [(16, digest), (4, PUBLIC[:4])], 208),
|
||||
(f"{PUBLIC.hex()},{PUBLIC.hex()}", [(16, digest)] * 2, 196)]:
|
||||
with self.subTest(token=token, maximum=maximum_length):
|
||||
prefix = "set flood.rule.1 "
|
||||
maximum = prefix + "a" * (maximum_length - len(prefix))
|
||||
expected = (envelope(maximum, target=digest) if entries is None
|
||||
else envelope2(entries, maximum))
|
||||
self.assertEqual(len(expected), 308)
|
||||
self.assertEqual(self.run_tool("encode2", CHANNEL.hex(), token, NOW, NOW + 120, maximum),
|
||||
expected.hex())
|
||||
self.assertNotEqual(self.decode(expected), "reject")
|
||||
# The fixture verifies no writes and no signature calls on
|
||||
# rejection, even when its output buffer exceeds packet size.
|
||||
oversized = maximum + "a"
|
||||
for operation in ["encode2", "encode2-wide"]:
|
||||
self.assertEqual(self.run_tool(operation, CHANNEL.hex(), token, NOW, NOW + 120,
|
||||
oversized), "reject")
|
||||
signed_oversized = (envelope(oversized, target=digest) if entries is None
|
||||
else envelope2(entries, oversized))
|
||||
self.assertEqual(self.decode(signed_oversized), "reject")
|
||||
altered = bytearray(expected)
|
||||
altered[-65] = ord("b")
|
||||
self.assertEqual(self.decode(altered), "reject")
|
||||
|
||||
def test_maximum_broadcast_signature_covers_final_byte_and_230_byte_length(self):
|
||||
prefix = "set flood.rule.1 "
|
||||
maximum = prefix + "a" * (230 - len(prefix))
|
||||
original = envelope2(command=maximum)
|
||||
self.assertNotEqual(self.decode(original), "reject")
|
||||
for command in [maximum[:-1] + "b", maximum[:-1]]:
|
||||
changed = envelope2(command=command)
|
||||
self.assertNotEqual(self.decode(changed), "reject")
|
||||
self.assertEqual(self.decode(changed[:-64] + original[-64:]), "reject")
|
||||
for length_byte in [0, 87, 229, 231, 255]:
|
||||
changed = bytearray(original)
|
||||
changed[13] = length_byte
|
||||
self.assertEqual(self.decode(changed), "reject")
|
||||
for offset in range(len(original) - 64, len(original)):
|
||||
with self.subTest(signature_byte=offset):
|
||||
changed = bytearray(original)
|
||||
changed[offset] ^= 1
|
||||
self.assertEqual(self.decode(changed), "reject")
|
||||
|
||||
def test_single_packet_boundaries_and_first_two_fragment_command(self):
|
||||
digest = hashlib.sha256(PUBLIC).digest()[:16]
|
||||
for token, entries, maximum in [
|
||||
("all", [], 87), (PUBLIC[:4].hex(), [(4, PUBLIC[:4])], 82),
|
||||
(PUBLIC[:6].hex(), [(6, PUBLIC[:6])], 80),
|
||||
(PUBLIC.hex(), None, 72)]:
|
||||
with self.subTest(token=token):
|
||||
prefix = "set flood.rule.1 "
|
||||
command = prefix + "a" * (maximum - len(prefix))
|
||||
single = (envelope(command, target=digest) if entries is None
|
||||
else envelope2(entries, command))
|
||||
self.assertEqual(len(single), 165)
|
||||
self.assertEqual(self.run_tool("encode2", CHANNEL.hex(), token, NOW, NOW + 120,
|
||||
command), single.hex())
|
||||
self.assertNotEqual(self.decode(single), "reject")
|
||||
for index in [0, 1]:
|
||||
self.assertEqual(self.run_tool("fragment", single.hex(), index, 165), "reject")
|
||||
longer = (envelope(command + "a", target=digest) if entries is None
|
||||
else envelope2(entries, command + "a"))
|
||||
self.assertEqual(len(longer), 166)
|
||||
self.assertEqual(self.run_tool("encode2", CHANNEL.hex(), token, NOW, NOW + 120,
|
||||
command + "a"), longer.hex())
|
||||
self.assertNotEqual(self.decode(longer), "reject")
|
||||
for index, size in [(0, 165), (1, 23)]:
|
||||
actual = bytes.fromhex(self.run_tool("fragment", longer.hex(), index, 165))
|
||||
self.assertEqual(actual, fragment_frame(longer, index))
|
||||
self.assertEqual(len(actual), size)
|
||||
|
||||
def test_two_fragments_reassemble_out_of_order_with_one_real_signature(self):
|
||||
for size in [166, 167, 307, 308]:
|
||||
with self.subTest(size=size):
|
||||
prefix = "set flood.rule.1 "
|
||||
command = prefix + "a" * (size - 78 - len(prefix))
|
||||
original = envelope2(command=command)
|
||||
self.assertEqual(len(original), size)
|
||||
# The fixture requires exactly one signature call for encode
|
||||
# and no signature calls while cutting either fragment.
|
||||
self.assertEqual(self.run_tool("encode2", CHANNEL.hex(), "all", NOW, NOW + 120,
|
||||
command), original.hex())
|
||||
parts = [bytes.fromhex(self.run_tool("fragment", original.hex(), index, 165))
|
||||
for index in [0, 1]]
|
||||
for index, part in enumerate(parts):
|
||||
self.assertEqual(part, fragment_frame(original, index))
|
||||
self.assertLessEqual(len(part), 165)
|
||||
self.assertEqual(self.run_tool("parse-fragment", part.hex()),
|
||||
f"{NOW}\n{size}\n{index}\n{part[11:].hex()}")
|
||||
self.assertEqual(self.decode(part), "reject")
|
||||
for order in [(0, 1), (1, 0), (0, 0, 1), (1, 1, 0)]:
|
||||
with self.subTest(order=order):
|
||||
assembly = bytearray(size)
|
||||
for index in order:
|
||||
part = parts[index]
|
||||
assembly[index * 154:index * 154 + len(part) - 11] = part[11:]
|
||||
assembly = bytes(assembly)
|
||||
self.assertEqual(assembly, original)
|
||||
PUBLISHER.public_key().verify(assembly[-64:], DOMAIN + CHANNEL + assembly[:-64])
|
||||
for node in [PUBLIC, OTHER_PUBLIC]:
|
||||
self.assertEqual(self.decode(assembly, self_key=node, metadata=True),
|
||||
f"{NOW}\n{NOW + 120}\n{command}\n1")
|
||||
|
||||
def test_fragment_codec_bounds_and_exact_shapes_reject_without_output_writes(self):
|
||||
prefix = "set flood.rule.1 "
|
||||
original = envelope2(command=prefix + "a" * (88 - len(prefix)))
|
||||
parts = [fragment_frame(original, index) for index in [0, 1]]
|
||||
for source in [b"", original[:165], original + b"a" * (309 - len(original)),
|
||||
b"FMX1" + original[4:], b"FMP1" + original[4:]]:
|
||||
with self.subTest(source_length=len(source), magic=source[:4]):
|
||||
self.assertEqual(self.run_tool("fragment", source.hex(), 0, 229), "reject")
|
||||
for index in [2, 255]:
|
||||
self.assertEqual(self.run_tool("fragment", original.hex(), index, 229), "reject")
|
||||
for index, required in [(0, 165), (1, 23)]:
|
||||
for capacity in [0, 10, required - 1]:
|
||||
self.assertEqual(self.run_tool("fragment", original.hex(), index, capacity), "reject")
|
||||
self.assertEqual(self.run_tool("fragment", original.hex(), index, 229), parts[index].hex())
|
||||
malformed = [b"", b"FMP1", parts[0][:10], parts[0][:11],
|
||||
parts[0][:-1], parts[0] + b"\0", parts[1][:-1], parts[1] + b"\0",
|
||||
b"FMX1" + parts[0][4:]]
|
||||
for total in [0, 154, 165, 309, 65535]:
|
||||
malformed.append(parts[0][:8] + struct.pack("<H", total) + parts[0][10:])
|
||||
for index in [1, 2, 255]:
|
||||
malformed.append(parts[0][:10] + bytes([index]) + parts[0][11:])
|
||||
malformed.append(parts[1][:10] + b"\0" + parts[1][11:])
|
||||
for part in malformed:
|
||||
with self.subTest(part=part.hex()):
|
||||
self.assertEqual(self.run_tool("parse-fragment", part.hex()), "reject")
|
||||
|
||||
def test_fragment_tampering_and_mixed_commands_cannot_forge_complete_signature(self):
|
||||
prefix = "set flood.rule.1 "
|
||||
command = prefix + "a" * (230 - len(prefix))
|
||||
original = envelope2(command=command)
|
||||
parts = [fragment_frame(original, index) for index in [0, 1]]
|
||||
self.assertNotEqual(self.decode(original), "reject")
|
||||
# Every chunk contains signed data. Matching frame metadata and an
|
||||
# allowed changed command cannot substitute for the full signature.
|
||||
for index, data_offset in [(0, 14 + len(prefix)), (1, 0), (1, 153)]:
|
||||
changed = bytearray(parts[index])
|
||||
changed[11 + data_offset] ^= 1
|
||||
self.assertNotEqual(self.run_tool("parse-fragment", changed.hex()), "reject")
|
||||
chunks = [parts[0][11:], parts[1][11:]]
|
||||
chunks[index] = changed[11:]
|
||||
self.assertEqual(self.decode(b"".join(chunks)), "reject")
|
||||
different = envelope2(command=prefix + "b" * (230 - len(prefix)))
|
||||
self.assertNotEqual(self.decode(different), "reject")
|
||||
different_parts = [fragment_frame(different, index) for index in [0, 1]]
|
||||
for combined in [parts[0][11:] + different_parts[1][11:],
|
||||
different_parts[0][11:] + parts[1][11:]]:
|
||||
self.assertEqual(self.decode(combined), "reject")
|
||||
# Frame metadata is deliberately unauthenticated and only admits a
|
||||
# bounded assembly. Runtime must compare it to the signed sequence.
|
||||
changed_sequence = parts[0][:4] + struct.pack("<I", NOW + 1) + parts[0][8:]
|
||||
self.assertTrue(self.run_tool("parse-fragment", changed_sequence.hex()).startswith(f"{NOW + 1}\n"))
|
||||
self.assertEqual(self.decode(changed_sequence), "reject")
|
||||
|
||||
def test_real_signature_round_trip_for_mixed_prefixes_and_full_keys(self):
|
||||
entries = [(4, PUBLIC[:4]), (6, OTHER_PUBLIC[:6]), (16, hashlib.sha256(PUBLIC).digest()[:16])]
|
||||
token = f"{PUBLIC[:4].hex()},{OTHER_PUBLIC[:6].hex()},{PUBLIC.hex()}"
|
||||
@@ -502,10 +755,10 @@ class FleetCommandCryptoTests(unittest.TestCase):
|
||||
self.assertEqual(self.run_tool("encode2", CHANNEL.hex(), token, NOW, NOW + 120, "get radio2"), "reject")
|
||||
|
||||
def test_fmc2_packet_capacity_is_checked_before_signing_or_sending(self):
|
||||
maximum = "set flood.rule.1 " + "a" * (72 - len("set flood.rule.1 "))
|
||||
maximum = "set flood.rule.1 " + "a" * (215 - len("set flood.rule.1 "))
|
||||
entries = [(4, PUBLIC[:4])] * 3
|
||||
expected = envelope2(entries, maximum)
|
||||
self.assertEqual(len(expected), 165)
|
||||
self.assertEqual(len(expected), 308)
|
||||
token = ",".join([PUBLIC[:4].hex()] * 3)
|
||||
actual = self.run_tool("encode2", CHANNEL.hex(), token, NOW, NOW + 120, maximum)
|
||||
self.assertEqual(actual, expected.hex())
|
||||
@@ -516,7 +769,7 @@ class FleetCommandCryptoTests(unittest.TestCase):
|
||||
for count in [15, 16, 17]:
|
||||
token = ",".join([PUBLIC[:4].hex()] * count)
|
||||
result = self.run_tool("encode2", CHANNEL.hex(), token, NOW, NOW + 120, "get radio2")
|
||||
self.assertEqual(result == "reject", count > 15)
|
||||
self.assertNotEqual(result, "reject")
|
||||
|
||||
def test_fmc2_rejects_short_trailing_and_signed_forbidden_commands(self):
|
||||
original = envelope2([(4, PUBLIC[:4]), (6, OTHER_PUBLIC[:6])])
|
||||
|
||||
Reference in new issue
Block a user