diff options
Diffstat (limited to 'packages/meshbay-node/tests/test_transport_wire_parity.py')
| -rw-r--r-- | packages/meshbay-node/tests/test_transport_wire_parity.py | 63 |
1 files changed, 60 insertions, 3 deletions
diff --git a/packages/meshbay-node/tests/test_transport_wire_parity.py b/packages/meshbay-node/tests/test_transport_wire_parity.py index 5e04525..bc6b134 100644 --- a/packages/meshbay-node/tests/test_transport_wire_parity.py +++ b/packages/meshbay-node/tests/test_transport_wire_parity.py @@ -19,6 +19,7 @@ import pytest from conftest import one_root from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.crypto import generate_gek +from meshbay_common.groupbox import PURPOSE_INDEX, unseal from meshbay_common.protocol import MNP, file_chunk_plaintext, file_chunk_wire from meshbay_node.indexer import DirectoryIndexer from meshbay_node.transport import quic_server, webrtc_server @@ -60,6 +61,37 @@ def test_both_transports_use_the_one_index_builder(): "QUIC is serializing the index again — that was the fork") +def test_neither_transport_seals_by_hand(): + """ + `groupbox` is the only sealer, the same rule `file_chunk_wire` already has. + A server reaching for AESGCM or HKDF directly is a second envelope waiting to + disagree with the first about a nonce length, an info string or an AAD. + """ + for module in (webrtc_server, quic_server): + source = inspect.getsource(module) + assert "seal(" in source, f"{module.__name__} sends an unsealed ack" + assert "AESGCM(" not in source, ( + f"{module.__name__} builds its own AEAD instead of using groupbox") + assert "HKDF(" not in source, ( + f"{module.__name__} derives its own subkey instead of using groupbox") + + +def test_the_daemon_does_not_build_an_index_message_itself(): + """ + The delta was hand-built in `_broadcast_index_change` — the third construction + site for an index message, and the one that would have kept sending cleartext + while the other two were sealed. + """ + from meshbay_node import daemon + + source = inspect.getsource(daemon) + assert "index_delta_message" in source and "index_sync_message" in source + assert '"type": MNP.INDEX_DELTA' not in source, ( + "the daemon builds index_delta by hand again") + assert '"type": MNP.INDEX_SYNC' not in source, ( + "the daemon builds index_sync by hand again") + + def test_chunk_wire_shape_is_identical_across_transports(gek, shared_dir): """The two servers' read-and-encrypt helpers agree on every field but the nonce.""" path = shared_dir / "film.mkv" @@ -116,10 +148,35 @@ async def test_index_sync_shape(gek, shared_dir): msg = index_sync_message(indexer.index, roots) + # In clear: what a receiver needs to route and version-check before it can + # decrypt, and nothing else. assert msg["type"] == MNP.INDEX_SYNC assert msg["group_id"] == "g" - assert [e["name"] for e in msg["entries"]] == ["film.mkv"] + assert set(msg) == {"type", "v", "group_id", "nonce", "ct"} + + payload = unseal(gek, PURPOSE_INDEX, MNP.INDEX_SYNC, "g", msg) + assert [e["name"] for e in payload["entries"]] == ["film.mkv"] # Directories are not index entries, so they travel separately — including the # empty one, which no entry's path would have revealed. - assert any(d.endswith("sub") for d in msg["dirs"]) - assert msg["roots"] + assert any(d.endswith("sub") for d in payload["dirs"]) + assert payload["roots"] + + +@pytest.mark.asyncio +async def test_a_wrong_key_raises_rather_than_reporting_an_empty_group(gek, shared_dir): + """ + §3.4, at the level a client would hit it. An index that fails to open must not + become an empty index: "the group has no files" is a legitimate state, so a + fallback there is indistinguishable from the truth — which is exactly what + makes it worse than a stop. + """ + sk_node = Ed25519PrivateKey.generate() + roots = one_root(shared_dir) + indexer = DirectoryIndexer(roots=roots, group_id="g", sk_node=sk_node, gek=gek) + await indexer.initial_scan() + + msg = index_sync_message(indexer.index, roots) + with pytest.raises(Exception) as caught: + unseal(generate_gek(), PURPOSE_INDEX, MNP.INDEX_SYNC, "g", msg) + # Assert on the refusal, not on a degraded result. + assert not isinstance(caught.value, dict) |