mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-21 13:55:24 -04:00
Two copies of test_proof_binds_error_reason and test_proof_binds_direction were sitting in the #else branch of test_ack_proof, where Identity, makeIdentity, makeAck, becomeNode, crypto and ACK_PROOF_SIZE do not exist. They were also unregistered, so they were dead code that only served to break the build. The native suite never compiles that branch, so a local run could not see it. Also apply clang-format to a declaration that had been wrapped by hand.
434 lines
20 KiB
C++
434 lines
20 KiB
C++
// PROTOTYPE suite for the pairwise ACK proof (src/mesh/AckProof.h).
|
|
//
|
|
// Covers the mechanism end to end - key agreement, request_id binding, wire encoding, and the
|
|
// verify verdicts - without standing up a NodeDB. The NodeDB-backed policy wrappers
|
|
// (ackProofAttach/ackProofVerify) and the ReliableRouter hook are deliberately NOT covered here;
|
|
// they need the full router harness, and this is a spike.
|
|
#include "configuration.h"
|
|
|
|
#include "TestUtil.h"
|
|
#include <unity.h>
|
|
|
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
|
|
|
#include "mesh/AckProof.h"
|
|
#include "mesh/CryptoEngine.h"
|
|
#include "mesh/MeshTypes.h"
|
|
#include <pb_decode.h>
|
|
#include <pb_encode.h>
|
|
|
|
static constexpr NodeNum ALICE = 0x0A0A0A0A; // sends the DM, verifies the ack
|
|
static constexpr NodeNum BOB = 0x0B0B0B0B; // receives the DM, produces the ack
|
|
static constexpr uint32_t REQUEST_ID = 0xABCD1234;
|
|
|
|
// Stand-in for the encoded Routing message the proof covers.
|
|
static const uint8_t ROUTING[] = {0x18, 0x00}; // error_reason = NONE
|
|
#define ROUTING_LEN sizeof(ROUTING)
|
|
|
|
struct Identity {
|
|
uint8_t pub[32];
|
|
uint8_t priv[32];
|
|
};
|
|
|
|
static Identity makeIdentity()
|
|
{
|
|
Identity id;
|
|
crypto->generateKeyPair(id.pub, id.priv);
|
|
return id;
|
|
}
|
|
|
|
/** Act as this node for subsequent crypto calls. */
|
|
static void becomeNode(const Identity &id)
|
|
{
|
|
uint8_t priv[32];
|
|
memcpy(priv, id.priv, sizeof(priv));
|
|
crypto->setDHPrivateKey(priv);
|
|
}
|
|
|
|
/** A minimal ROUTING ack, shaped the way MeshModule::allocAckNak builds one. */
|
|
static meshtastic_MeshPacket makeAck(NodeNum from, NodeNum to, uint32_t requestId)
|
|
{
|
|
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
|
|
p.from = from;
|
|
p.to = to;
|
|
p.id = 0x5150;
|
|
p.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
p.decoded.portnum = meshtastic_PortNum_ROUTING_APP;
|
|
p.decoded.request_id = requestId;
|
|
|
|
meshtastic_Routing ack = meshtastic_Routing_init_default;
|
|
ack.which_variant = meshtastic_Routing_error_reason_tag;
|
|
ack.error_reason = meshtastic_Routing_Error_NONE;
|
|
p.decoded.payload.size =
|
|
pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_Routing_msg, &ack);
|
|
TEST_ASSERT_GREATER_THAN(0, p.decoded.payload.size);
|
|
return p;
|
|
}
|
|
|
|
void setUp(void) {}
|
|
void tearDown(void) {}
|
|
|
|
// The whole scheme rests on this: both endpoints derive the same value from opposite halves of the
|
|
// key pair, and nobody else can.
|
|
void test_proof_is_pairwise_and_symmetric(void)
|
|
{
|
|
const Identity alice = makeIdentity();
|
|
const Identity bob = makeIdentity();
|
|
const Identity mallory = makeIdentity();
|
|
|
|
uint8_t fromBob[ACK_PROOF_SIZE], fromAlice[ACK_PROOF_SIZE], fromMallory[ACK_PROOF_SIZE];
|
|
|
|
// Both endpoints compute over the SAME direction tuple (Bob is the acker); only the key half
|
|
// they hold differs.
|
|
becomeNode(bob);
|
|
TEST_ASSERT_TRUE(crypto->ackProofCompute(alice.pub, BOB, ALICE, REQUEST_ID, ROUTING, ROUTING_LEN, fromBob));
|
|
becomeNode(alice);
|
|
TEST_ASSERT_TRUE(crypto->ackProofCompute(bob.pub, BOB, ALICE, REQUEST_ID, ROUTING, ROUTING_LEN, fromAlice));
|
|
TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(fromBob, fromAlice, ACK_PROOF_SIZE,
|
|
"both endpoints must derive the same proof from the shared secret");
|
|
|
|
// A third party holding both public keys cannot reproduce it.
|
|
becomeNode(mallory);
|
|
TEST_ASSERT_TRUE(crypto->ackProofCompute(alice.pub, BOB, ALICE, REQUEST_ID, ROUTING, ROUTING_LEN, fromMallory));
|
|
TEST_ASSERT_TRUE_MESSAGE(memcmp(fromBob, fromMallory, ACK_PROOF_SIZE) != 0, "a non-peer must not derive the proof");
|
|
}
|
|
|
|
// request_id is an input, so a captured proof cannot be aimed at a different pending packet - the
|
|
// retargeting problem that the signature scheme needed an explicit buffer change to solve.
|
|
void test_proof_binds_request_id(void)
|
|
{
|
|
const Identity alice = makeIdentity();
|
|
const Identity bob = makeIdentity();
|
|
|
|
uint8_t a[ACK_PROOF_SIZE], b[ACK_PROOF_SIZE];
|
|
becomeNode(bob);
|
|
TEST_ASSERT_TRUE(crypto->ackProofCompute(alice.pub, BOB, ALICE, REQUEST_ID, ROUTING, ROUTING_LEN, a));
|
|
TEST_ASSERT_TRUE(crypto->ackProofCompute(alice.pub, BOB, ALICE, REQUEST_ID + 1, ROUTING, ROUTING_LEN, b));
|
|
TEST_ASSERT_TRUE_MESSAGE(memcmp(a, b, ACK_PROOF_SIZE) != 0, "proof must differ per request_id");
|
|
}
|
|
|
|
// Without an identity there is nothing to prove with, and we must fail rather than emit a constant.
|
|
void test_proof_requires_private_key(void)
|
|
{
|
|
const Identity peer = makeIdentity();
|
|
uint8_t zero[32] = {0}, proof[ACK_PROOF_SIZE];
|
|
crypto->setDHPrivateKey(zero);
|
|
TEST_ASSERT_FALSE(crypto->ackProofCompute(peer.pub, BOB, ALICE, REQUEST_ID, ROUTING, ROUTING_LEN, proof));
|
|
}
|
|
|
|
// An all-zero peer key is a weak point; Curve25519::dh2 rejects it and so must we.
|
|
void test_proof_rejects_weak_peer_key(void)
|
|
{
|
|
const Identity self = makeIdentity();
|
|
becomeNode(self);
|
|
uint8_t zeroPub[32] = {0}, proof[ACK_PROOF_SIZE];
|
|
TEST_ASSERT_FALSE(crypto->ackProofCompute(zeroPub, BOB, ALICE, REQUEST_ID, ROUTING, ROUTING_LEN, proof));
|
|
}
|
|
|
|
// The proof rides as a protobuf field appended to the encoded Routing payload. A decoder still
|
|
// reads the ack correctly with it present - which held when ack_proof was an unknown field to this
|
|
// build and must keep holding now that it is generated, since older firmware sees it as unknown.
|
|
void test_wire_roundtrip_and_unknown_field_tolerance(void)
|
|
{
|
|
const Identity alice = makeIdentity();
|
|
const Identity bob = makeIdentity();
|
|
|
|
becomeNode(bob);
|
|
meshtastic_MeshPacket ack = makeAck(BOB, ALICE, REQUEST_ID);
|
|
const pb_size_t bare = ack.decoded.payload.size;
|
|
TEST_ASSERT_TRUE(ackProofAttachWithKey(&ack, alice.pub));
|
|
|
|
// Encoded cost is the proof plus a tag and a length byte, and only on acks.
|
|
TEST_ASSERT_EQUAL_MESSAGE(bare + ACK_PROOF_SIZE + 2, ack.decoded.payload.size, "proof must cost exactly 10 encoded bytes");
|
|
|
|
uint8_t extracted[ACK_PROOF_SIZE], expected[ACK_PROOF_SIZE];
|
|
TEST_ASSERT_TRUE(ackProofExtract(ack.decoded.payload.bytes, ack.decoded.payload.size, extracted));
|
|
uint8_t bareRouting[64];
|
|
memcpy(bareRouting, ack.decoded.payload.bytes, bare);
|
|
TEST_ASSERT_TRUE(crypto->ackProofCompute(alice.pub, BOB, ALICE, REQUEST_ID, bareRouting, bare, expected));
|
|
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, extracted, ACK_PROOF_SIZE);
|
|
|
|
// Parsing the ack is unaffected by the proof - the case older firmware, which does not know the
|
|
// field, will be in.
|
|
meshtastic_Routing decoded = meshtastic_Routing_init_default;
|
|
TEST_ASSERT_TRUE_MESSAGE(
|
|
pb_decode_from_bytes(ack.decoded.payload.bytes, ack.decoded.payload.size, &meshtastic_Routing_msg, &decoded),
|
|
"a decoder must still parse the Routing message with a proof present");
|
|
TEST_ASSERT_EQUAL(meshtastic_Routing_error_reason_tag, decoded.which_variant);
|
|
TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, decoded.error_reason);
|
|
}
|
|
|
|
// A bare ack (every current firmware) is ABSENT, not INVALID - this is what keeps the scheme
|
|
// deployable alongside nodes that know nothing about it.
|
|
void test_verify_verdicts(void)
|
|
{
|
|
const Identity alice = makeIdentity();
|
|
const Identity bob = makeIdentity();
|
|
const Identity mallory = makeIdentity();
|
|
|
|
becomeNode(bob);
|
|
meshtastic_MeshPacket bare = makeAck(BOB, ALICE, REQUEST_ID);
|
|
meshtastic_MeshPacket proven = makeAck(BOB, ALICE, REQUEST_ID);
|
|
TEST_ASSERT_TRUE(ackProofAttachWithKey(&proven, alice.pub));
|
|
|
|
becomeNode(alice);
|
|
TEST_ASSERT_EQUAL(AckProofResult::ABSENT, ackProofVerifyWithKey(&bare, REQUEST_ID, bob.pub));
|
|
TEST_ASSERT_EQUAL(AckProofResult::VALID, ackProofVerifyWithKey(&proven, REQUEST_ID, bob.pub));
|
|
|
|
// Present but unjudgeable, because we hold no key for the acker.
|
|
TEST_ASSERT_EQUAL(AckProofResult::NO_KEY, ackProofVerifyWithKey(&proven, REQUEST_ID, nullptr));
|
|
|
|
// Right proof, wrong pending packet: retargeting is caught.
|
|
TEST_ASSERT_EQUAL(AckProofResult::INVALID, ackProofVerifyWithKey(&proven, REQUEST_ID + 1, bob.pub));
|
|
|
|
// Same ack attributed to a different signer's key.
|
|
TEST_ASSERT_EQUAL(AckProofResult::INVALID, ackProofVerifyWithKey(&proven, REQUEST_ID, mallory.pub));
|
|
}
|
|
|
|
// A forger with the channel PSK can rewrite the ack, but cannot produce the proof.
|
|
void test_verify_rejects_tampered_proof(void)
|
|
{
|
|
const Identity alice = makeIdentity();
|
|
const Identity bob = makeIdentity();
|
|
|
|
becomeNode(bob);
|
|
meshtastic_MeshPacket ack = makeAck(BOB, ALICE, REQUEST_ID);
|
|
TEST_ASSERT_TRUE(ackProofAttachWithKey(&ack, alice.pub));
|
|
|
|
becomeNode(alice);
|
|
TEST_ASSERT_EQUAL(AckProofResult::VALID, ackProofVerifyWithKey(&ack, REQUEST_ID, bob.pub));
|
|
ack.decoded.payload.bytes[ack.decoded.payload.size - 1] ^= 0x01;
|
|
TEST_ASSERT_EQUAL(AckProofResult::INVALID, ackProofVerifyWithKey(&ack, REQUEST_ID, bob.pub));
|
|
}
|
|
|
|
// A truncated or over-long field is malformed, not a proof - an honest sender emits exactly
|
|
// ACK_PROOF_SIZE bytes, so anything else is rejected outright rather than compared.
|
|
void test_malformed_proof_field_is_absent(void)
|
|
{
|
|
meshtastic_MeshPacket ack = makeAck(BOB, ALICE, REQUEST_ID);
|
|
uint8_t junk[ACK_PROOF_SIZE - 1];
|
|
memset(junk, 0x5A, sizeof(junk));
|
|
|
|
pb_ostream_t stream = pb_ostream_from_buffer(ack.decoded.payload.bytes + ack.decoded.payload.size,
|
|
sizeof(ack.decoded.payload.bytes) - ack.decoded.payload.size);
|
|
TEST_ASSERT_TRUE(pb_encode_tag(&stream, PB_WT_STRING, ACK_PROOF_FIELD_NUMBER));
|
|
TEST_ASSERT_TRUE(pb_encode_string(&stream, junk, sizeof(junk)));
|
|
ack.decoded.payload.size += stream.bytes_written;
|
|
|
|
uint8_t out[ACK_PROOF_SIZE];
|
|
TEST_ASSERT_FALSE(ackProofExtract(ack.decoded.payload.bytes, ack.decoded.payload.size, out));
|
|
}
|
|
|
|
// Nothing is attached to packets that are not explicit acks, so no other traffic pays for this.
|
|
void test_attach_only_applies_to_routing_unicasts(void)
|
|
{
|
|
const Identity peer = makeIdentity();
|
|
const Identity self = makeIdentity();
|
|
becomeNode(self);
|
|
|
|
meshtastic_MeshPacket broadcast = makeAck(BOB, NODENUM_BROADCAST, REQUEST_ID);
|
|
TEST_ASSERT_FALSE_MESSAGE(ackProofAttachWithKey(&broadcast, peer.pub), "broadcasts have no single peer to prove to");
|
|
|
|
meshtastic_MeshPacket noRequestId = makeAck(BOB, ALICE, 0);
|
|
TEST_ASSERT_FALSE_MESSAGE(ackProofAttachWithKey(&noRequestId, peer.pub), "nothing to bind without a request_id");
|
|
|
|
meshtastic_MeshPacket text = makeAck(BOB, ALICE, REQUEST_ID);
|
|
text.decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
TEST_ASSERT_FALSE_MESSAGE(ackProofAttachWithKey(&text, peer.pub), "only ROUTING acks carry a proof");
|
|
}
|
|
|
|
// THE load-bearing property for multi-hop: a repeater that holds the channel key relays by
|
|
// decoding and re-encoding the Data message (perhapsDecode decodes in place, then perhapsEncode
|
|
// re-runs pb_encode on p->decoded for any packet not from us). That cycle strips unknown fields at
|
|
// the Data level - so the proof survives only because it is nested inside Data.payload, which is an
|
|
// opaque bytes field the decoder copies through verbatim. Both halves are asserted here: if anyone
|
|
// ever "simplifies" this by moving the proof up alongside xeddsa_signature, the second half fails.
|
|
void test_proof_survives_relay_reencode(void)
|
|
{
|
|
const Identity alice = makeIdentity();
|
|
const Identity bob = makeIdentity();
|
|
|
|
becomeNode(bob);
|
|
meshtastic_MeshPacket ack = makeAck(BOB, ALICE, REQUEST_ID);
|
|
TEST_ASSERT_TRUE(ackProofAttachWithKey(&ack, alice.pub));
|
|
|
|
// A field appended at the *Data* level, as a control for what a hop destroys.
|
|
uint8_t control[4] = {0xDE, 0xAD, 0xBE, 0xEF};
|
|
uint8_t ctrlBuf[16];
|
|
pb_ostream_t ctrl = pb_ostream_from_buffer(ctrlBuf, sizeof(ctrlBuf));
|
|
TEST_ASSERT_TRUE(pb_encode_tag(&ctrl, PB_WT_STRING, 101)); // not a field of meshtastic_Data
|
|
TEST_ASSERT_TRUE(pb_encode_string(&ctrl, control, sizeof(control)));
|
|
|
|
uint8_t wire[512];
|
|
const size_t wireLen = pb_encode_to_bytes(wire, sizeof(wire), &meshtastic_Data_msg, &ack.decoded);
|
|
TEST_ASSERT_GREATER_THAN(0, wireLen);
|
|
memcpy(wire + wireLen, ctrlBuf, ctrl.bytes_written);
|
|
const size_t withControl = wireLen + ctrl.bytes_written;
|
|
|
|
// Exactly what a repeater does with what it received.
|
|
meshtastic_MeshPacket relayed = meshtastic_MeshPacket_init_zero;
|
|
relayed.from = BOB;
|
|
relayed.to = ALICE;
|
|
relayed.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
TEST_ASSERT_TRUE(pb_decode_from_bytes(wire, withControl, &meshtastic_Data_msg, &relayed.decoded));
|
|
uint8_t reencoded[512];
|
|
const size_t reencodedLen = pb_encode_to_bytes(reencoded, sizeof(reencoded), &meshtastic_Data_msg, &relayed.decoded);
|
|
TEST_ASSERT_GREATER_THAN(0, reencodedLen);
|
|
|
|
// The Data-level field is gone after one hop...
|
|
TEST_ASSERT_LESS_THAN_MESSAGE(withControl, reencodedLen, "a Data-level unknown field must NOT survive the relay re-encode");
|
|
|
|
// ...but the proof, nested in the payload blob, is still there and still verifies downstream.
|
|
meshtastic_MeshPacket downstream = meshtastic_MeshPacket_init_zero;
|
|
downstream.from = BOB;
|
|
downstream.to = ALICE;
|
|
downstream.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
TEST_ASSERT_TRUE(pb_decode_from_bytes(reencoded, reencodedLen, &meshtastic_Data_msg, &downstream.decoded));
|
|
TEST_ASSERT_EQUAL(REQUEST_ID, downstream.decoded.request_id);
|
|
|
|
becomeNode(alice);
|
|
TEST_ASSERT_EQUAL_MESSAGE(AckProofResult::VALID, ackProofVerifyWithKey(&downstream, REQUEST_ID, bob.pub),
|
|
"proof must survive a repeater's decode/re-encode cycle");
|
|
}
|
|
|
|
// REGRESSION: an ack and a nak for the same packet share from/to/portnum/request_id and differ
|
|
// only in the Routing payload. Channel crypto is CTR with no MAC, so if the payload were not bound
|
|
// a PSK holder could bit-flip a proven "delivered" into a "failed" and the proof would still
|
|
// verify. The easy direction is success -> failure; manufacturing a fake success is much narrower,
|
|
// needing a captured proofed nak, and the naks that exist either never reach the air or come from
|
|
// a node that by definition holds no key.
|
|
void test_proof_binds_error_reason(void)
|
|
{
|
|
const Identity alice = makeIdentity();
|
|
const Identity bob = makeIdentity();
|
|
|
|
becomeNode(bob);
|
|
meshtastic_MeshPacket ack = makeAck(BOB, ALICE, REQUEST_ID);
|
|
TEST_ASSERT_EQUAL_MESSAGE(0x18, ack.decoded.payload.bytes[0], "expected error_reason to be field 3, varint");
|
|
TEST_ASSERT_EQUAL_MESSAGE(meshtastic_Routing_Error_NONE, ack.decoded.payload.bytes[1], "expected a success ack");
|
|
TEST_ASSERT_TRUE(ackProofAttachWithKey(&ack, alice.pub));
|
|
|
|
becomeNode(alice);
|
|
TEST_ASSERT_EQUAL(AckProofResult::VALID, ackProofVerifyWithKey(&ack, REQUEST_ID, bob.pub));
|
|
|
|
// Exactly what a CTR bit-flip buys the attacker: same proof bytes, different verdict.
|
|
ack.decoded.payload.bytes[1] = meshtastic_Routing_Error_MAX_RETRANSMIT;
|
|
TEST_ASSERT_EQUAL_MESSAGE(AckProofResult::INVALID, ackProofVerifyWithKey(&ack, REQUEST_ID, bob.pub),
|
|
"flipping a proven ack into a nak must invalidate the proof");
|
|
}
|
|
|
|
// X25519 is symmetric - DH(a_priv, B_pub) == DH(b_priv, A_pub) - so without the direction bound an
|
|
// A->B proof for a request_id would equal the B->A proof for it. Cheap insurance; this pins it.
|
|
void test_proof_binds_direction(void)
|
|
{
|
|
const Identity alice = makeIdentity();
|
|
const Identity bob = makeIdentity();
|
|
|
|
becomeNode(bob);
|
|
uint8_t forward[ACK_PROOF_SIZE], reverse[ACK_PROOF_SIZE];
|
|
TEST_ASSERT_TRUE(crypto->ackProofCompute(alice.pub, BOB, ALICE, REQUEST_ID, ROUTING, ROUTING_LEN, forward));
|
|
TEST_ASSERT_TRUE(crypto->ackProofCompute(alice.pub, ALICE, BOB, REQUEST_ID, ROUTING, ROUTING_LEN, reverse));
|
|
TEST_ASSERT_TRUE_MESSAGE(memcmp(forward, reverse, ACK_PROOF_SIZE) != 0,
|
|
"A->B and B->A proofs must differ despite the symmetric shared secret");
|
|
}
|
|
|
|
// Two proof fields mean two possible excisions and so two possible verdicts. Refuse rather than
|
|
// silently pick one, since an attacker with the channel key can append a second field at will.
|
|
void test_duplicate_proof_field_is_rejected(void)
|
|
{
|
|
const Identity alice = makeIdentity();
|
|
const Identity bob = makeIdentity();
|
|
|
|
becomeNode(bob);
|
|
meshtastic_MeshPacket ack = makeAck(BOB, ALICE, REQUEST_ID);
|
|
TEST_ASSERT_TRUE(ackProofAttachWithKey(&ack, alice.pub));
|
|
|
|
uint8_t first[ACK_PROOF_SIZE];
|
|
TEST_ASSERT_TRUE(ackProofExtract(ack.decoded.payload.bytes, ack.decoded.payload.size, first));
|
|
|
|
// Append a second, byte-identical proof field.
|
|
pb_ostream_t stream = pb_ostream_from_buffer(ack.decoded.payload.bytes + ack.decoded.payload.size,
|
|
sizeof(ack.decoded.payload.bytes) - ack.decoded.payload.size);
|
|
TEST_ASSERT_TRUE(pb_encode_tag(&stream, PB_WT_STRING, ACK_PROOF_FIELD_NUMBER));
|
|
TEST_ASSERT_TRUE(pb_encode_string(&stream, first, ACK_PROOF_SIZE));
|
|
ack.decoded.payload.size += stream.bytes_written;
|
|
|
|
uint8_t out[ACK_PROOF_SIZE];
|
|
TEST_ASSERT_FALSE_MESSAGE(ackProofExtract(ack.decoded.payload.bytes, ack.decoded.payload.size, out),
|
|
"a message with two proof fields must be rejected as malformed");
|
|
|
|
becomeNode(alice);
|
|
TEST_ASSERT_EQUAL(AckProofResult::ABSENT, ackProofVerifyWithKey(&ack, REQUEST_ID, bob.pub));
|
|
}
|
|
|
|
// The proof must cover fields this build does not understand, so that a future Routing field
|
|
// outside the oneof is protected and old and new nodes still agree on the hashed bytes. Excision
|
|
// gives that for free; re-encoding from the decoded struct would silently drop them.
|
|
void test_proof_covers_unknown_routing_fields(void)
|
|
{
|
|
const Identity alice = makeIdentity();
|
|
const Identity bob = makeIdentity();
|
|
|
|
becomeNode(bob);
|
|
meshtastic_MeshPacket ack = makeAck(BOB, ALICE, REQUEST_ID);
|
|
|
|
// A field this build's Routing schema does not define, as a newer sender would emit.
|
|
const pb_size_t beforeUnknown = ack.decoded.payload.size;
|
|
uint8_t extra[3] = {0x11, 0x22, 0x33};
|
|
pb_ostream_t pre = pb_ostream_from_buffer(ack.decoded.payload.bytes + ack.decoded.payload.size,
|
|
sizeof(ack.decoded.payload.bytes) - ack.decoded.payload.size);
|
|
TEST_ASSERT_TRUE(pb_encode_tag(&pre, PB_WT_STRING, 9)); // not a field of meshtastic_Routing
|
|
TEST_ASSERT_TRUE(pb_encode_string(&pre, extra, sizeof(extra)));
|
|
ack.decoded.payload.size += pre.bytes_written;
|
|
// Its contents start after the one-byte tag and one-byte length.
|
|
const size_t unknownDataStart = beforeUnknown + 2;
|
|
|
|
TEST_ASSERT_TRUE(ackProofAttachWithKey(&ack, alice.pub));
|
|
|
|
becomeNode(alice);
|
|
TEST_ASSERT_EQUAL(AckProofResult::VALID, ackProofVerifyWithKey(&ack, REQUEST_ID, bob.pub));
|
|
|
|
// Tamper with the field's CONTENTS, not its framing: the message must stay parseable so that
|
|
// the verdict is INVALID (the proof no longer matches) rather than ABSENT (no readable proof).
|
|
ack.decoded.payload.bytes[unknownDataStart] ^= 0x01;
|
|
TEST_ASSERT_EQUAL_MESSAGE(AckProofResult::INVALID, ackProofVerifyWithKey(&ack, REQUEST_ID, bob.pub),
|
|
"an unknown field must be covered by the proof");
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_proof_is_pairwise_and_symmetric);
|
|
RUN_TEST(test_proof_binds_request_id);
|
|
RUN_TEST(test_proof_binds_error_reason);
|
|
RUN_TEST(test_proof_binds_direction);
|
|
RUN_TEST(test_proof_requires_private_key);
|
|
RUN_TEST(test_proof_rejects_weak_peer_key);
|
|
RUN_TEST(test_wire_roundtrip_and_unknown_field_tolerance);
|
|
RUN_TEST(test_verify_verdicts);
|
|
RUN_TEST(test_verify_rejects_tampered_proof);
|
|
RUN_TEST(test_malformed_proof_field_is_absent);
|
|
RUN_TEST(test_duplicate_proof_field_is_rejected);
|
|
RUN_TEST(test_proof_covers_unknown_routing_fields);
|
|
RUN_TEST(test_attach_only_applies_to_routing_unicasts);
|
|
RUN_TEST(test_proof_survives_relay_reencode);
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
void loop() {}
|
|
|
|
#else // PKI excluded
|
|
|
|
void setUp(void) {}
|
|
void tearDown(void) {}
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
UNITY_BEGIN();
|
|
exit(UNITY_END());
|
|
}
|
|
void loop() {}
|
|
|
|
#endif
|