tests: cover keyless Data envelopes and session confirmation

Steady-state traffic uses Data envelopes, without sender keys nor credential, once the peer answered a request. Data envelopes only open from the session address, also authorize writes, and survive the receiver's epoch rotation.
This commit is contained in:
Adrien Beraud committed 2026-10-29 23:28:48 -04:00
1 parent 853bb1eeed
commit 40560eec4e
3 files changed
+114 -18

No files matched your search

+71 -17
View File
@@ -1085,6 +1085,15 @@ NetworkEngineTester::testTransportEncryptsAfterKeyExchange()
CPPUNIT_ASSERT(isEnvelope(b.socket->sends.at(0), transport::EnvelopeType::Session));
b.deliverTo(a);
CPPUNIT_ASSERT(done);
// b answered our request: both have the session, keys are left out
done = false;
a.ping(a.node(b), done);
CPPUNIT_ASSERT(isEnvelope(a.socket->sends.at(0), transport::EnvelopeType::Data));
a.deliverTo(b);
CPPUNIT_ASSERT(isEnvelope(b.socket->sends.at(0), transport::EnvelopeType::Data));
b.deliverTo(a);
CPPUNIT_ASSERT(done);
}
void
@@ -1259,9 +1268,8 @@ NetworkEngineTester::testTransportPrivateNetwork()
CPPUNIT_ASSERT(done);
auto peerB = a.node(b)->getTransport();
CPPUNIT_ASSERT(peerB and peerB->isAuthorized(std::chrono::system_clock::now()));
// b only saw our credential in the unauthenticated hello
CPPUNIT_ASSERT(not peerB->acked);
// b only saw our credential in the unauthenticated hello
done = false;
a.ping(a.node(b), done);
CPPUNIT_ASSERT(isEnvelope(a.socket->sends.at(0), transport::EnvelopeType::Session, true));
@@ -1269,15 +1277,13 @@ NetworkEngineTester::testTransportPrivateNetwork()
CPPUNIT_ASSERT(isEnvelope(b.socket->sends.at(0), transport::EnvelopeType::Session, true));
b.deliverTo(a);
CPPUNIT_ASSERT(done);
CPPUNIT_ASSERT(peerB->acked);
// b accepted our credential: no need to send it anymore
// b accepted our request: Data envelopes, without credential
done = false;
a.ping(a.node(b), done);
CPPUNIT_ASSERT(isEnvelope(a.socket->sends.at(0), transport::EnvelopeType::Session));
CPPUNIT_ASSERT(isEnvelope(a.socket->sends.at(0), transport::EnvelopeType::Data));
a.deliverTo(b);
// we haven't replied to b yet: it still sends its credential
CPPUNIT_ASSERT(isEnvelope(b.socket->sends.at(0), transport::EnvelopeType::Session, true));
CPPUNIT_ASSERT(isEnvelope(b.socket->sends.at(0), transport::EnvelopeType::Data));
b.deliverTo(a);
CPPUNIT_ASSERT(done);
}
@@ -1350,31 +1356,52 @@ NetworkEngineTester::testTransportEpochRotation()
auto captured = a.socket->sends.at(0);
CPPUNIT_ASSERT(isEnvelope(captured, transport::EnvelopeType::Session));
a.deliverTo(b);
auto oldReply = b.socket->sends.at(0);
b.deliverTo(a);
a.ping(a.node(b), done);
auto capturedData = a.socket->sends.at(0);
CPPUNIT_ASSERT(isEnvelope(capturedData, transport::EnvelopeType::Data));
a.deliverTo(b);
b.deliverTo(a);
advance(a, std::chrono::minutes(30));
// b rotates: a's Data envelopes still open with b's previous epoch
advance(b, std::chrono::minutes(30));
a.takeSent();
b.takeSent();
done = false;
a.ping(a.node(b), done);
CPPUNIT_ASSERT(isEnvelope(a.socket->sends.at(0), transport::EnvelopeType::Data));
a.deliverTo(b);
// b answers with its new ephemeral key, until a confirms the new session
CPPUNIT_ASSERT(isEnvelope(b.socket->sends.at(0), transport::EnvelopeType::Session));
CPPUNIT_ASSERT(ephemeralKey(b.socket->sends.at(0)) != ephemeralKey(oldReply));
b.deliverTo(a);
CPPUNIT_ASSERT(done);
done = false;
a.ping(a.node(b), done);
CPPUNIT_ASSERT(isEnvelope(a.socket->sends.at(0), transport::EnvelopeType::Data));
a.deliverTo(b);
CPPUNIT_ASSERT(isEnvelope(b.socket->sends.at(0), transport::EnvelopeType::Data));
b.deliverTo(a);
CPPUNIT_ASSERT(done);
// a uses a new ephemeral key, with the one b used before rotating
// a rotates: new ephemeral key
advance(a, std::chrono::minutes(30));
a.takeSent();
done = false;
a.ping(a.node(b), done);
CPPUNIT_ASSERT(isEnvelope(a.socket->sends.at(0), transport::EnvelopeType::Session));
CPPUNIT_ASSERT(ephemeralKey(a.socket->sends.at(0)) != ephemeralKey(captured));
a.deliverTo(b);
CPPUNIT_ASSERT(isEnvelope(b.socket->sends.at(0), transport::EnvelopeType::Session));
b.deliverTo(a);
CPPUNIT_ASSERT(done);
// The previous ephemeral key is still accepted for one period
// Keys of the previous epoch are accepted for one period, then erased
b.engine->processMessage(captured.data.data(), captured.data.size(), a.addr);
CPPUNIT_ASSERT_EQUAL((size_t) 1, b.takeSent().size());
// then erased
advance(b, std::chrono::minutes(30));
b.takeSent();
b.engine->processMessage(captured.data.data(), captured.data.size(), a.addr);
b.engine->processMessage(capturedData.data.data(), capturedData.data.size(), a.addr);
CPPUNIT_ASSERT(b.socket->sends.empty());
}
@@ -1396,13 +1423,12 @@ NetworkEngineTester::testTransportPeerRestart()
b->deliverTo(a);
CPPUNIT_ASSERT(done);
}
CPPUNIT_ASSERT(a.node(*b)->getTransport()->acked);
// b restarts with the same key: it knows neither our ephemeral key nor our credential
// b restarts with the same key: it lost the session and our credential
b = std::make_unique<TransportTestNode>("127.0.0.3", configB, 3);
done = false;
a.ping(a.node(*b), done);
CPPUNIT_ASSERT(isEnvelope(a.socket->sends.at(0), transport::EnvelopeType::Session));
CPPUNIT_ASSERT(isEnvelope(a.socket->sends.at(0), transport::EnvelopeType::Data));
a.deliverTo(*b);
CPPUNIT_ASSERT(b->socket->sends.empty());
@@ -1505,6 +1531,23 @@ NetworkEngineTester::testTransportSessionAuthorizesWrites()
CPPUNIT_ASSERT_EQUAL(1, b.listens);
b.deliverTo(a);
CPPUNIT_ASSERT(token and token->empty());
// Data envelopes also come from the address that got b's ephemeral key
token.reset();
a.engine->sendListen(a.node(b),
InfoHash::getRandom(rd),
{},
{},
2,
[&token](const net::Request&, net::RequestAnswer&& answer) {
token = std::move(answer.ntoken);
},
{});
CPPUNIT_ASSERT(isEnvelope(a.socket->sends.at(0), transport::EnvelopeType::Data));
a.deliverTo(b);
CPPUNIT_ASSERT_EQUAL(2, b.listens);
b.deliverTo(a);
CPPUNIT_ASSERT(token and token->empty());
}
void
@@ -1578,6 +1621,17 @@ NetworkEngineTester::testTransportEphemeralBoundToAddress()
CPPUNIT_ASSERT(b.socket->sends.empty());
b.engine->processMessage(fromA.data.data(), fromA.data.size(), a.addr);
CPPUNIT_ASSERT_EQUAL((size_t) 1, b.socket->sends.size());
// So does a Data envelope, without any key derivation
a.takeSent();
b.deliverTo(a);
a.ping(a.node(b), done);
auto data = a.socket->sends.at(0);
CPPUNIT_ASSERT(isEnvelope(data, transport::EnvelopeType::Data));
b.engine->processMessage(data.data.data(), data.data.size(), c.addr);
CPPUNIT_ASSERT(b.socket->sends.empty());
b.engine->processMessage(data.data.data(), data.data.size(), a.addr);
CPPUNIT_ASSERT_EQUAL((size_t) 1, b.socket->sends.size());
}
} // namespace test
+41 -1
View File
@@ -195,6 +195,46 @@ TransportTester::testOpenRejectsTampering()
CPPUNIT_ASSERT(not open(sessionKeys(b, eb, a.pub, ea.pub, 1)->rx, envelope.data(), envelope.size()));
}
void
TransportTester::testDataEnvelope()
{
auto a = KeyPair::generate(), ea = KeyPair::generate();
auto b = KeyPair::generate(), eb = KeyPair::generate();
auto keys = *sessionKeys(a, ea, b.pub, eb.pub, 0);
auto msg = message();
auto envelope = sealData(keys.tx, msg.data(), msg.size());
CPPUNIT_ASSERT_EQUAL(msg.size() + DATA_ENVELOPE_OVERHEAD, envelope.size());
CPPUNIT_ASSERT_EQUAL((size_t) 30, DATA_ENVELOPE_OVERHEAD);
CPPUNIT_ASSERT(isEnvelope(envelope.data(), envelope.size()));
CPPUNIT_ASSERT_THROW(msgpack::unpack((const char*) envelope.data(), envelope.size()), std::exception);
auto header = parseHeader(envelope.data(), envelope.size());
CPPUNIT_ASSERT(header and header->type == EnvelopeType::Data and not header->credential);
auto opened = open(sessionKeys(b, eb, a.pub, ea.pub, 0)->rx, envelope.data(), envelope.size());
CPPUNIT_ASSERT(opened and *opened == msg);
for (size_t i : {(size_t) 1, DATA_HEADER_SIZE, DATA_HEADER_SIZE + NONCE_SIZE, envelope.size() - 1}) {
auto tampered = envelope;
tampered[i] ^= 0x01;
CPPUNIT_ASSERT(not open(keys.tx, tampered.data(), tampered.size()));
}
CPPUNIT_ASSERT(not open(keys.tx, envelope.data(), DATA_ENVELOPE_OVERHEAD - 1));
CPPUNIT_ASSERT(not open(keys.rx, envelope.data(), envelope.size()));
// Same key, other type: the type is authenticated
auto session = seal(keys.tx, EnvelopeType::Session, a.pub, ea.pub, msg.data(), msg.size());
Blob relabeled {ENVELOPE_MARKER, static_cast<uint8_t>(EnvelopeType::Data)};
relabeled.insert(relabeled.end(), session.begin() + HEADER_SIZE, session.end());
CPPUNIT_ASSERT(not open(keys.tx, relabeled.data(), relabeled.size()));
// Data never carries a credential
auto flagged = envelope;
flagged[1] |= CREDENTIAL_FLAG;
CPPUNIT_ASSERT(not parseHeader(flagged.data(), flagged.size()));
CPPUNIT_ASSERT(not open(keys.tx, flagged.data(), flagged.size()));
}
void
TransportTester::testParseHeader()
{
@@ -203,7 +243,7 @@ TransportTester::testParseHeader()
auto envelope = seal(Key {}, EnvelopeType::Session, a.pub, ea.pub, msg.data(), msg.size());
CPPUNIT_ASSERT(parseHeader(envelope.data(), HEADER_SIZE));
CPPUNIT_ASSERT(not parseHeader(envelope.data(), HEADER_SIZE - 1));
for (uint8_t type : {0, 4, 0x80, 0x84}) {
for (uint8_t type : {0, 5, 0x80, 0x84, 0x85}) {
auto unknown = envelope;
unknown[1] = type;
CPPUNIT_ASSERT(not parseHeader(unknown.data(), unknown.size()));
+2
View File
@@ -18,6 +18,7 @@ class TransportTester : public CppUnit::TestFixture
CPPUNIT_TEST(testAddressKey);
CPPUNIT_TEST(testSealOpen);
CPPUNIT_TEST(testOpenRejectsTampering);
CPPUNIT_TEST(testDataEnvelope);
CPPUNIT_TEST(testParseHeader);
CPPUNIT_TEST(testEnvelopeIsNotMsgpack);
CPPUNIT_TEST(testHello);
@@ -37,6 +38,7 @@ public:
void testAddressKey();
void testSealOpen();
void testOpenRejectsTampering();
void testDataEnvelope();
void testParseHeader();
void testEnvelopeIsNotMsgpack();
void testHello();