From 2d4779c2516272fe8d5d0f5a5009f738f6e62c86 Mon Sep 17 00:00:00 2001 From: Colin Constable Date: Thu, 2 Jul 2026 16:11:44 -0700 Subject: [PATCH 1/7] fix: random IV for stored keys (put/get), matching the Dart SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously put/get encrypted every stored value under a static all-zero IV (the default of aes_encrypt/decrypt_from_base64) — IV reuse across all data. Match the Dart at_client behavior: - shared keys: put always generates a random 16-byte IV, stored as ivNonce in the key metadata (serialized into the update command); - self keys: keep the legacy zero IV unless the caller set ivNonce (Dart does not auto-generate for self keys); - get: fetch metadata via llookup:all / lookup:all, use ivNonce when present, else fall back to the legacy zero IV — so existing data (and Dart-written legacy data) still decrypts; - Metadata: keep iv_nonce as raw bytes internally (decode incoming base64) so it round-trips with generate_iv_nonce()/__str__. Selection is purely by presence of ivNonce in metadata, matching Dart. Network-free unit tests in test/put_get_iv_test.py; cross-SDK interop verified separately against the Dart reference SDK. --- at_client/atclient.py | 59 ++++++++++++++++++----------- at_client/common/metadata.py | 11 ++++-- test/put_get_iv_test.py | 73 ++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 24 deletions(-) create mode 100644 test/put_get_iv_test.py diff --git a/at_client/atclient.py b/at_client/atclient.py index 810f354..8162b39 100644 --- a/at_client/atclient.py +++ b/at_client/atclient.py @@ -21,6 +21,11 @@ from .common.keys import AtKey, Keys, SharedKey, PrivateHiddenKey, PublicKey, SelfKey from .util.authutil import AuthUtil +# The 16-byte all-zero IV used before random IVs existed. Matches the Dart SDK's +# AtChopsUtil.generateIVLegacy() / getIV(null); used to decrypt legacy data (no ivNonce). +LEGACY_IV = b"\x00" * 16 + + class AtClient(ABC): def __init__(self, atsign:AtSign, root_address:Address=Address("root.atsign.org", 64), secondary_address:Address=None, queue:Queue=None, verbose:bool = False): self.atsign = atsign @@ -186,8 +191,11 @@ def put(self, key, value): def _put_self_key(self, key: SelfKey, value: str): key.metadata.data_signature = EncryptionUtil.sign_sha256_rsa(value, self.keys[KeysUtil.encryption_private_key_name]) + # Match the Dart SDK: self keys use the legacy zero IV unless the caller has + # already set an ivNonce (Dart does NOT auto-generate one for self keys). + iv = key.metadata.iv_nonce if key.metadata.iv_nonce is not None else LEGACY_IV try: - cipher_text = EncryptionUtil.aes_encrypt_from_base64(value, self.keys[KeysUtil.self_encryption_key_name]) + cipher_text = EncryptionUtil.aes_encrypt_from_base64(value, self.keys[KeysUtil.self_encryption_key_name], iv) except Exception as e: raise AtEncryptionException(f"Failed to encrypt value with self encryption key - {e}") @@ -218,7 +226,11 @@ def _put_shared_key(self, key: SharedKey, value: str): share_to_encryption_key = self.get_encryption_key_shared_by_me(key) what = "encrypt value with shared encryption key" - cipher_text = EncryptionUtil.aes_encrypt_from_base64(value, share_to_encryption_key) + # Match the Dart SDK: shared keys always get a random IV, persisted as + # ivNonce in the key metadata (which is serialized into the update command). + if key.metadata.iv_nonce is None: + key.metadata.iv_nonce = EncryptionUtil.generate_iv_nonce() + cipher_text = EncryptionUtil.aes_encrypt_from_base64(value, share_to_encryption_key, key.metadata.iv_nonce) except Exception as e: raise AtEncryptionException(f"Failed to {what} - {e}") @@ -255,6 +267,17 @@ def get_lookup_response(self, command: str): return fetched + @staticmethod + def _iv_from_fetched(fetched) -> bytes: + """Read the IV from a fetched (all) lookup response's metadata. + + Returns the ivNonce bytes if present, else the legacy all-zero IV — matching + the Dart SDK's `metadata.ivNonce != null ? fromBase64 : generateIVLegacy()`. + """ + meta = fetched.get("metaData") or {} + iv_b64 = meta.get("ivNonce") + return base64.b64decode(iv_b64) if iv_b64 else LEGACY_IV + def _get_self_key(self, key: SelfKey): command = LlookupVerbBuilder().with_at_key(key, LlookupVerbBuilder.Type.ALL).build() @@ -263,8 +286,9 @@ def _get_self_key(self, key: SelfKey): decrypted_value = None encrypted_value = fetched["data"] self_encryption_key = self.keys[KeysUtil.self_encryption_key_name] + iv = self._iv_from_fetched(fetched) try: - decrypted_value = EncryptionUtil.aes_decrypt_from_base64(encrypted_value, self_encryption_key) + decrypted_value = EncryptionUtil.aes_decrypt_from_base64(encrypted_value, self_encryption_key, iv) except Exception as e: raise AtDecryptionException(f"Failed to {command} - {e}") @@ -296,38 +320,31 @@ def _get_shared_key(self, key: SharedKey): def _get_shared_by_me_with_other(self, shared_key: SharedKey): share_encryption_key = self.get_encryption_key_shared_by_me(shared_key) - raw_response = None - command = "llookup:" + str(shared_key) - try: - raw_response = self.secondary_connection.execute_command(command, True) - except (AtKeyNotFoundException, AtInternalServerException) as e: raise e - except AtSecondaryConnectException as e: - raise AtSecondaryConnectException(f"Failed to execute {command} - {e}") + # Use llookup:all so we get the metadata (ivNonce) alongside the value. + command = "llookup:all:" + str(shared_key) + fetched = self.get_lookup_response(command) + iv = self._iv_from_fetched(fetched) try: - return EncryptionUtil.aes_decrypt_from_base64(raw_response.get_raw_data_response(), share_encryption_key) + return EncryptionUtil.aes_decrypt_from_base64(fetched["data"], share_encryption_key, iv) except Exception as e: raise AtDecryptionException(f"Failed to decrypt value with shared encryption key - {e}") def _get_shared_by_other_with_me(self, shared_key:SharedKey): - what = None share_encryption_key = self.get_encryption_key_shared_by_other(shared_key) - raw_response = None - command = "lookup:" + shared_key.name + # Use lookup:all so we get the metadata (ivNonce) alongside the value. + command = "lookup:all:" + shared_key.name if shared_key.get_namespace() is not None and shared_key.get_namespace(): command += "." + shared_key.get_namespace() command += str(shared_key.shared_by) - try: - raw_response = self.secondary_connection.execute_command(command, True) - except Exception as e: - raise AtSecondaryConnectException(f"Failed to execute {command} - {e}") + fetched = self.get_lookup_response(command) + iv = self._iv_from_fetched(fetched) - what = "decrypt value with shared encryption key" try: - return EncryptionUtil.aes_decrypt_from_base64(raw_response.get_raw_data_response(), share_encryption_key) + return EncryptionUtil.aes_decrypt_from_base64(fetched["data"], share_encryption_key, iv) except Exception as e: - raise AtDecryptionException(f"Failed to {what} - {e}") + raise AtDecryptionException(f"Failed to decrypt value with shared encryption key - {e}") def delete(self, key): if isinstance(key, SharedKey) or isinstance(key, SelfKey) or isinstance(key, PublicKey): diff --git a/at_client/common/metadata.py b/at_client/common/metadata.py index 53eda98..1600397 100644 --- a/at_client/common/metadata.py +++ b/at_client/common/metadata.py @@ -68,8 +68,11 @@ def from_json(json_str): metadata.shared_key_enc = data.get('sharedKeyEnc') metadata.pub_key_cs = data.get('pubKeyCS') metadata.encoding = data.get('encoding') - metadata.iv_nonce = data.get('ivNonce') - + # ivNonce travels as base64 on the wire; keep it as raw bytes internally so it + # round-trips with generate_iv_nonce()/__str__ and is usable directly as an IV. + _iv = data.get('ivNonce') + metadata.iv_nonce = binascii.a2b_base64(_iv) if _iv else None + return metadata @staticmethod @@ -100,7 +103,9 @@ def from_dict(data_dict): metadata.shared_key_enc = data_dict.get('sharedKeyEnc') metadata.pub_key_cs = data_dict.get('pubKeyCS') metadata.encoding = data_dict.get('encoding') - metadata.iv_nonce = data_dict.get('ivNonce') + # ivNonce travels as base64; keep raw bytes internally (see from_json). + _iv = data_dict.get('ivNonce') + metadata.iv_nonce = binascii.a2b_base64(_iv) if _iv else None return metadata diff --git a/test/put_get_iv_test.py b/test/put_get_iv_test.py new file mode 100644 index 0000000..5fcc842 --- /dev/null +++ b/test/put_get_iv_test.py @@ -0,0 +1,73 @@ +import base64 +import unittest +from unittest.mock import MagicMock + +from at_client import AtClient +from at_client.atclient import LEGACY_IV +from at_client.common import AtSign +from at_client.common.keys import SharedKey +from at_client.common.metadata import Metadata +from at_client.util.encryptionutil import EncryptionUtil + + +class PutGetIVTest(unittest.TestCase): + """Network-free tests for random-IV put/get (Dart-matched behavior).""" + + def test_aes_roundtrip_with_random_iv(self): + key = EncryptionUtil.generate_aes_key_base64() + iv = EncryptionUtil.generate_iv_nonce() + enc = EncryptionUtil.aes_encrypt_from_base64("hello world", key, iv) + dec = EncryptionUtil.aes_decrypt_from_base64(enc.encode(), key, iv) + self.assertEqual(dec, "hello world") + + def test_legacy_zero_iv_is_16_bytes(self): + self.assertEqual(LEGACY_IV, b"\x00" * 16) + + def test_metadata_iv_nonce_bytes_roundtrip(self): + iv = EncryptionUtil.generate_iv_nonce() # bytes + md = Metadata() + md.iv_nonce = iv + # __str__ emits base64 + b64 = base64.b64encode(iv).decode() + self.assertIn(f":ivNonce:{b64}", str(md)) + # from_json decodes base64 back to the SAME bytes + parsed = Metadata.from_json(f'{{"ivNonce":"{b64}"}}') + self.assertEqual(parsed.iv_nonce, iv) + + def test_iv_from_fetched(self): + iv = EncryptionUtil.generate_iv_nonce() + b64 = base64.b64encode(iv).decode() + self.assertEqual(AtClient._iv_from_fetched({"metaData": {"ivNonce": b64}}), iv) + self.assertEqual(AtClient._iv_from_fetched({"metaData": {}}), LEGACY_IV) + self.assertEqual(AtClient._iv_from_fetched({}), LEGACY_IV) + + def test_put_shared_key_generates_and_persists_iv(self): + client = AtClient.__new__(AtClient) + me = AtSign("@alice") + client.atsign = me + client.get_encryption_key_shared_by_me = MagicMock( + return_value=EncryptionUtil.generate_aes_key_base64()) + sent = {} + resp = MagicMock() + resp.get_raw_data_response.return_value = "data:ok" + + def _exec(command, *a, **k): + sent["command"] = command + return resp + client.secondary_connection = MagicMock() + client.secondary_connection.execute_command.side_effect = _exec + + key = SharedKey("demo", me, AtSign("@bob")) + key.set_namespace("test") + self.assertIsNone(key.metadata.iv_nonce) + + client._put_shared_key(key, "secret") + + self.assertIsInstance(key.metadata.iv_nonce, (bytes, bytearray)) + self.assertEqual(len(key.metadata.iv_nonce), 16) # random 16-byte IV + b64 = base64.b64encode(key.metadata.iv_nonce).decode() + self.assertIn(f":ivNonce:{b64}", sent["command"]) # persisted in update cmd + + +if __name__ == "__main__": + unittest.main() From 1a1d6633af5af273640818ab7acdaaf16385eba7 Mon Sep 17 00:00:00 2001 From: Colin Constable Date: Thu, 2 Jul 2026 16:52:46 -0700 Subject: [PATCH 2/7] fix: random IV for self keys too + carry ivNonce through UpdateVerbBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self keys now also get a random IV (stored as ivNonce), not just shared keys. Dart's current SelfKeyEncryption uses the zero IV for self keys, which is the same IV-reuse weakness; this is interop-safe because get falls back to the legacy zero IV when ivNonce is absent, and Dart's self decrypt honors ivNonce when present. Required fixing UpdateVerbBuilder, which silently dropped iv_nonce (set_metadata and _build_metadata_str never carried it) — so a self-key ivNonce would not have been persisted. Now round-trips. Tests: self put generates+persists a 16-byte IV; self encrypt->decrypt via ivNonce. --- at_client/atclient.py | 12 ++++++--- at_client/util/verbbuilder.py | 2 ++ test/put_get_iv_test.py | 48 +++++++++++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/at_client/atclient.py b/at_client/atclient.py index 8162b39..b593828 100644 --- a/at_client/atclient.py +++ b/at_client/atclient.py @@ -191,11 +191,15 @@ def put(self, key, value): def _put_self_key(self, key: SelfKey, value: str): key.metadata.data_signature = EncryptionUtil.sign_sha256_rsa(value, self.keys[KeysUtil.encryption_private_key_name]) - # Match the Dart SDK: self keys use the legacy zero IV unless the caller has - # already set an ivNonce (Dart does NOT auto-generate one for self keys). - iv = key.metadata.iv_nonce if key.metadata.iv_nonce is not None else LEGACY_IV + # Generate a random IV per self key too (stored as ivNonce in metadata). Dart's + # current SelfKeyEncryption does NOT do this (it uses the zero IV) — that's a + # security gap on the Dart side; doing it here is interop-safe because get falls + # back to the legacy zero IV when ivNonce is absent, and Dart's self decrypt + # already honors ivNonce when present. + if key.metadata.iv_nonce is None: + key.metadata.iv_nonce = EncryptionUtil.generate_iv_nonce() try: - cipher_text = EncryptionUtil.aes_encrypt_from_base64(value, self.keys[KeysUtil.self_encryption_key_name], iv) + cipher_text = EncryptionUtil.aes_encrypt_from_base64(value, self.keys[KeysUtil.self_encryption_key_name], key.metadata.iv_nonce) except Exception as e: raise AtEncryptionException(f"Failed to encrypt value with self encryption key - {e}") diff --git a/at_client/util/verbbuilder.py b/at_client/util/verbbuilder.py index adea2c4..b74341f 100644 --- a/at_client/util/verbbuilder.py +++ b/at_client/util/verbbuilder.py @@ -181,6 +181,7 @@ def set_metadata(self, metadata): self.shared_key_enc = metadata.shared_key_enc self.pub_key_cs = metadata.pub_key_cs self.encoding = metadata.encoding + self.iv_nonce = metadata.iv_nonce return self def with_at_key(self, at_key, value): @@ -228,6 +229,7 @@ def _build_metadata_str(self): metadata.shared_key_enc = self.shared_key_enc metadata.pub_key_cs = self.pub_key_cs metadata.encoding = self.encoding + metadata.iv_nonce = getattr(self, "iv_nonce", None) return str(metadata) class LlookupVerbBuilder: diff --git a/test/put_get_iv_test.py b/test/put_get_iv_test.py index 5fcc842..7bc4240 100644 --- a/test/put_get_iv_test.py +++ b/test/put_get_iv_test.py @@ -1,13 +1,14 @@ import base64 import unittest -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from at_client import AtClient from at_client.atclient import LEGACY_IV from at_client.common import AtSign -from at_client.common.keys import SharedKey +from at_client.common.keys import SelfKey, SharedKey from at_client.common.metadata import Metadata from at_client.util.encryptionutil import EncryptionUtil +from at_client.util.keysutil import KeysUtil class PutGetIVTest(unittest.TestCase): @@ -68,6 +69,49 @@ def _exec(command, *a, **k): b64 = base64.b64encode(key.metadata.iv_nonce).decode() self.assertIn(f":ivNonce:{b64}", sent["command"]) # persisted in update cmd + def test_put_self_key_generates_and_persists_iv(self): + client = AtClient.__new__(AtClient) + me = AtSign("@alice") + client.atsign = me + client.keys = { + KeysUtil.self_encryption_key_name: EncryptionUtil.generate_aes_key_base64(), + KeysUtil.encryption_private_key_name: "x", # sign is patched below; value unused + } + sent = {} + resp = MagicMock() + resp.get_raw_data_response.return_value = "data:ok" + + def _exec(command, *a, **k): + sent["command"] = command + return resp + client.secondary_connection = MagicMock() + client.secondary_connection.execute_command.side_effect = _exec + + key = SelfKey("selfdemo", me) + key.set_namespace("test") + with patch("at_client.atclient.EncryptionUtil.sign_sha256_rsa", return_value="sig"): + client._put_self_key(key, "self secret") + + self.assertEqual(len(key.metadata.iv_nonce), 16) # random IV generated + b64 = base64.b64encode(key.metadata.iv_nonce).decode() + self.assertIn(f":ivNonce:{b64}", sent["command"]) # persisted via UpdateVerbBuilder + + def test_self_key_roundtrip_with_random_iv(self): + """Encrypt as put-self does, then decrypt as get-self does — via ivNonce.""" + self_key = EncryptionUtil.generate_aes_key_base64() + iv = EncryptionUtil.generate_iv_nonce() + cipher = EncryptionUtil.aes_encrypt_from_base64("self value", self_key, iv) + + client = AtClient.__new__(AtClient) + client.secondary_connection = None # silence __del__ during GC + client.keys = {KeysUtil.self_encryption_key_name: self_key} + fetched = {"key": "selfdemo.test@alice", "data": cipher, + "metaData": {"ivNonce": base64.b64encode(iv).decode()}} + client.get_lookup_response = MagicMock(return_value=fetched) + + k = SelfKey("selfdemo", AtSign("@alice")); k.set_namespace("test") + self.assertEqual(client._get_self_key(k), "self value") + if __name__ == "__main__": unittest.main() From 2bc6618d13fded54b2ae0450e73606c1bdcee782 Mon Sep 17 00:00:00 2001 From: Colin Constable Date: Thu, 2 Jul 2026 17:10:24 -0700 Subject: [PATCH 3/7] refactor: generate IV in put() (mirror Dart AtClientImpl._putInternal) Move IV generation into put(), before dispatch to the per-type encryptor, matching Dart's structure (it randomizes ivNonce for every put in _putInternal). Keeps the per-type ??= backstop so direct calls stay safe. Adds a test for the put() layer. --- at_client/atclient.py | 10 +++++++++- test/put_get_iv_test.py | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/at_client/atclient.py b/at_client/atclient.py index b593828..f57cc99 100644 --- a/at_client/atclient.py +++ b/at_client/atclient.py @@ -179,6 +179,13 @@ def get_encryption_key_shared_by_other(self, shared_key: SharedKey): def put(self, key, value): + # Generate the IV once here, before dispatching to the per-type encryptor — + # mirroring the Dart SDK's AtClientImpl._putInternal, which sets a random ivNonce + # for every put ahead of encryption. This guarantees every encrypted put gets a + # random IV regardless of key type; the per-type _put_* methods keep a `?=` + # backstop so direct calls are safe too. Public keys aren't encrypted, so no IV. + if isinstance(key, (SharedKey, SelfKey)) and key.metadata.iv_nonce is None: + key.metadata.iv_nonce = EncryptionUtil.generate_iv_nonce() if isinstance(key, SharedKey): return self._put_shared_key(key, value) elif isinstance(key, SelfKey): @@ -198,8 +205,9 @@ def _put_self_key(self, key: SelfKey, value: str): # already honors ivNonce when present. if key.metadata.iv_nonce is None: key.metadata.iv_nonce = EncryptionUtil.generate_iv_nonce() + self_key = self.keys[KeysUtil.self_encryption_key_name] try: - cipher_text = EncryptionUtil.aes_encrypt_from_base64(value, self.keys[KeysUtil.self_encryption_key_name], key.metadata.iv_nonce) + cipher_text = EncryptionUtil.aes_encrypt_from_base64(value, self_key, key.metadata.iv_nonce) except Exception as e: raise AtEncryptionException(f"Failed to encrypt value with self encryption key - {e}") diff --git a/test/put_get_iv_test.py b/test/put_get_iv_test.py index 7bc4240..08e729b 100644 --- a/test/put_get_iv_test.py +++ b/test/put_get_iv_test.py @@ -96,6 +96,25 @@ def _exec(command, *a, **k): b64 = base64.b64encode(key.metadata.iv_nonce).decode() self.assertIn(f":ivNonce:{b64}", sent["command"]) # persisted via UpdateVerbBuilder + def test_put_layer_generates_iv_before_dispatch(self): + """put() itself sets the IV (like Dart's _putInternal), before the encryptor.""" + client = AtClient.__new__(AtClient) + client._put_shared_key = MagicMock(return_value="ok") + client._put_self_key = MagicMock(return_value="ok") + me = AtSign("@alice") + + sk = SharedKey("k", me, AtSign("@bob")); sk.set_namespace("test") + self.assertIsNone(sk.metadata.iv_nonce) + client.put(sk, "v") + self.assertEqual(len(sk.metadata.iv_nonce), 16) + client._put_shared_key.assert_called_once() + + selfk = SelfKey("k", me); selfk.set_namespace("test") + self.assertIsNone(selfk.metadata.iv_nonce) + client.put(selfk, "v") + self.assertEqual(len(selfk.metadata.iv_nonce), 16) + client._put_self_key.assert_called_once() + def test_self_key_roundtrip_with_random_iv(self): """Encrypt as put-self does, then decrypt as get-self does — via ivNonce.""" self_key = EncryptionUtil.generate_aes_key_base64() From 55a411e7ee5ccf88c70318ade072bf0e919a65b0 Mon Sep 17 00:00:00 2001 From: Colin Constable Date: Thu, 2 Jul 2026 17:19:29 -0700 Subject: [PATCH 4/7] test: add guarded cross-SDK IV interop test (Python <-> Dart) + opt-in CI test/interop_test.py exercises random-IV shared-key put/get both directions against the Dart reference at_client. Skipped unless AT_INTEROP=1 and Dart is on PATH, so the normal unittest run and fork CI are unaffected. Dart helper + pubspec under test/interop/. Draft manual workflow in .github/workflows/interop.yml starts the ephemeral env, onboards two atSigns, and runs it. --- .github/workflows/interop.yml | 82 +++ test/interop/.dart_tool/package_config.json | 400 ++++++++++++++ test/interop/.dart_tool/package_graph.json | 560 ++++++++++++++++++++ test/interop/README.md | 35 ++ test/interop/bin/iv_interop.dart | 83 +++ test/interop/pubspec.lock | 517 ++++++++++++++++++ test/interop/pubspec.yaml | 11 + test/interop_test.py | 102 ++++ 8 files changed, 1790 insertions(+) create mode 100644 .github/workflows/interop.yml create mode 100644 test/interop/.dart_tool/package_config.json create mode 100644 test/interop/.dart_tool/package_graph.json create mode 100644 test/interop/README.md create mode 100644 test/interop/bin/iv_interop.dart create mode 100644 test/interop/pubspec.lock create mode 100644 test/interop/pubspec.yaml create mode 100644 test/interop_test.py diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml new file mode 100644 index 0000000..3ab7e8c --- /dev/null +++ b/.github/workflows/interop.yml @@ -0,0 +1,82 @@ +# DRAFT — opt-in cross-SDK IV interop test (Python at_client <-> Dart at_client). +# Manual only (workflow_dispatch). Review before enabling on PRs: it starts the +# ephemeral environment container, onboards two test atSigns via CRAM, installs the +# SDK, and runs test/interop_test.py with AT_INTEROP=1. +name: interop (Python <-> Dart) + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + interop: + runs-on: ubuntu-latest + timeout-minutes: 20 + services: + ee: + image: atsigncompany/ephemeral + env: + DNS_FQDN: vip.ve.atsign.zone + FIRST_PORT: 2500 + ports: + - 64:64 + - 2500-2540:2500-2540 + steps: + - uses: actions/checkout@v4 + + - name: Resolve the EE FQDN to localhost (matches the cert CN) + run: echo "127.0.0.1 vip.ve.atsign.zone" | sudo tee -a /etc/hosts + + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - uses: dart-lang/setup-dart@v1 + + - name: Install the SDK (repo generates README.PyPI.md at publish time) + run: | + cp README.md README.PyPI.md + pip install . + + - name: Wait for the EE root, then onboard @alpha and @bravo (CRAM) + run: | + export HOME=/tmp/eehome + mkdir -p "$HOME/.atsign/keys" + # give the root time to register the atServers + sleep 30 + cid=$(docker ps --filter ancestor=atsigncompany/ephemeral --format '{{.ID}}' | head -1) + python - "$cid" <<'PY' + import subprocess, sys + from at_client.connections import Address, AtRootConnection, AtSecondaryConnection + from at_client.common import AtSign + from at_client.util import AuthUtil, OnboardingUtil, KeysUtil + cid = sys.argv[1] + root = Address.from_string("vip.ve.atsign.zone:64") + for name in ("alpha", "bravo"): + atsign = AtSign("@" + name) + cram = subprocess.check_output( + ["docker", "exec", cid, "cat", f"/atsign/atservers/{name}/CRAM"]).decode().strip() + sec = AtRootConnection.get_instance(root.host, root.port).find_secondary(atsign) + conn = AtSecondaryConnection(sec); conn.connect() + auth, ob = AuthUtil(), OnboardingUtil() + auth.authenticate_with_cram(conn, atsign, cram) + keys = {} + ob.generate_self_encryption_key(keys); ob.generate_pkam_keypair(keys) + ob.generate_encryption_keypair(keys); KeysUtil.save_keys(atsign, keys) + ob.store_pkam_public_key(conn, keys) + auth.authenticate_with_pkam(conn, atsign, KeysUtil.load_keys(atsign)) + ob.store_public_encryption_key(conn, atsign.without_prefix, keys) + ob.delete_cram_key(conn) + print("onboarded @" + name) + PY + + - name: dart pub get (interop helper) + run: dart pub get + working-directory: test/interop + + - name: Run interop test + run: | + HOME=/tmp/eehome AT_INTEROP=1 \ + AT_ROOT=vip.ve.atsign.zone:64 AT_ROOT_DOMAIN=vip.ve.atsign.zone \ + python -m unittest discover -s test -p 'interop_test.py' -v diff --git a/test/interop/.dart_tool/package_config.json b/test/interop/.dart_tool/package_config.json new file mode 100644 index 0000000..7507f6b --- /dev/null +++ b/test/interop/.dart_tool/package_config.json @@ -0,0 +1,400 @@ +{ + "configVersion": 2, + "packages": [ + { + "name": "archive", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/archive-4.0.9", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "args", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/args-2.7.0", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "asn1lib", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/asn1lib-1.6.5", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "async", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/async-2.13.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "at_auth", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_auth-3.1.0", + "packageUri": "lib/", + "languageVersion": "3.6" + }, + { + "name": "at_base2e15", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_base2e15-1.0.0", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "at_chops", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_chops-3.3.0", + "packageUri": "lib/", + "languageVersion": "3.6" + }, + { + "name": "at_cli_commons", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_cli_commons-2.2.0", + "packageUri": "lib/", + "languageVersion": "3.6" + }, + { + "name": "at_client", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_client-3.12.0", + "packageUri": "lib/", + "languageVersion": "3.6" + }, + { + "name": "at_commons", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_commons-5.11.0", + "packageUri": "lib/", + "languageVersion": "3.6" + }, + { + "name": "at_lookup", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_lookup-3.5.0", + "packageUri": "lib/", + "languageVersion": "3.6" + }, + { + "name": "at_onboarding_cli", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_onboarding_cli-1.15.0", + "packageUri": "lib/", + "languageVersion": "3.6" + }, + { + "name": "at_persistence_secondary_server", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_persistence_secondary_server-4.3.5", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "at_persistence_spec", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_persistence_spec-3.1.0", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "at_server_status", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_server_status-1.1.1", + "packageUri": "lib/", + "languageVersion": "3.6" + }, + { + "name": "at_utf7", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_utf7-1.0.0", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "at_utils", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/at_utils-3.4.0", + "packageUri": "lib/", + "languageVersion": "3.6" + }, + { + "name": "better_cryptography", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/better_cryptography-1.0.0+1", + "packageUri": "lib/", + "languageVersion": "2.17" + }, + { + "name": "chalkdart", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/chalkdart-3.1.0", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "charcode", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/charcode-1.4.0", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "clock", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/clock-1.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "collection", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/collection-1.19.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "convert", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/convert-3.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "cron", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/cron-0.5.1", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "crypto", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/crypto-3.0.7", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "cryptography", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/cryptography-2.9.0", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "crypton", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/crypton-2.2.1", + "packageUri": "lib/", + "languageVersion": "2.18" + }, + { + "name": "dart_periphery", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/dart_periphery-0.9.20", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "duration", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/duration-4.0.3", + "packageUri": "lib/", + "languageVersion": "2.17" + }, + { + "name": "ecdsa", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/ecdsa-0.1.2", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "elliptic", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/elliptic-0.3.12", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "encrypt", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/encrypt-5.0.3", + "packageUri": "lib/", + "languageVersion": "2.18" + }, + { + "name": "ffi", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/ffi-2.2.0", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "file", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/file-7.0.1", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "fixnum", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/fixnum-1.1.1", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "hive", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/hive-2.2.3", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "http", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/http-1.6.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "http_parser", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/http_parser-4.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "image", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/image-4.9.1", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "internet_connection_checker", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/internet_connection_checker-1.0.0+1", + "packageUri": "lib/", + "languageVersion": "2.18" + }, + { + "name": "js", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/js-0.6.7", + "packageUri": "lib/", + "languageVersion": "2.19" + }, + { + "name": "json_annotation", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/json_annotation-4.12.0", + "packageUri": "lib/", + "languageVersion": "3.9" + }, + { + "name": "logging", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/logging-1.3.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "meta", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/meta-1.18.3", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "mutex", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/mutex-3.1.0", + "packageUri": "lib/", + "languageVersion": "2.15" + }, + { + "name": "ninja_asn1", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/ninja_asn1-2.0.0", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "path", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/path-1.9.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "petitparser", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/petitparser-7.0.2", + "packageUri": "lib/", + "languageVersion": "3.8" + }, + { + "name": "platform", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/platform-3.1.6", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "pointycastle", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/pointycastle-3.9.1", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "posix", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/posix-6.5.0", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "pqcrypto", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/pqcrypto-0.3.1", + "packageUri": "lib/", + "languageVersion": "3.10" + }, + { + "name": "process", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/process-5.0.5", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "source_span", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/source_span-1.10.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "stack_trace", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/stack_trace-1.12.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "string_scanner", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/string_scanner-1.4.1", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "term_glyph", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/term_glyph-1.2.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "typed_data", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/typed_data-1.4.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "uuid", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/uuid-4.5.3", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "version", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/version-3.0.2", + "packageUri": "lib/", + "languageVersion": "2.12" + }, + { + "name": "web", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/web-1.1.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "xml", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/xml-7.0.1", + "packageUri": "lib/", + "languageVersion": "3.11" + }, + { + "name": "yaml", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/yaml-3.1.3", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "zxing2", + "rootUri": "file:///tmp/eehome/.pub-cache/hosted/pub.dev/zxing2-0.2.4", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "at_python_interop", + "rootUri": "../", + "packageUri": "lib/", + "languageVersion": "3.0" + } + ], + "generator": "pub", + "generatorVersion": "3.11.5", + "flutterRoot": "file:///Users/cconstab/flutter-arm64/flutter", + "flutterVersion": "3.41.9", + "pubCache": "file:///tmp/eehome/.pub-cache" +} diff --git a/test/interop/.dart_tool/package_graph.json b/test/interop/.dart_tool/package_graph.json new file mode 100644 index 0000000..6b04949 --- /dev/null +++ b/test/interop/.dart_tool/package_graph.json @@ -0,0 +1,560 @@ +{ + "roots": [ + "at_python_interop" + ], + "packages": [ + { + "name": "at_python_interop", + "version": "0.0.0", + "dependencies": [ + "args", + "at_cli_commons", + "at_client" + ], + "devDependencies": [] + }, + { + "name": "at_cli_commons", + "version": "2.2.0", + "dependencies": [ + "args", + "at_client", + "at_onboarding_cli", + "at_utils", + "chalkdart", + "logging", + "meta", + "path", + "version" + ] + }, + { + "name": "version", + "version": "3.0.2", + "dependencies": [] + }, + { + "name": "args", + "version": "2.7.0", + "dependencies": [] + }, + { + "name": "path", + "version": "1.9.1", + "dependencies": [] + }, + { + "name": "logging", + "version": "1.3.0", + "dependencies": [] + }, + { + "name": "at_client", + "version": "3.12.0", + "dependencies": [ + "archive", + "async", + "at_auth", + "at_base2e15", + "at_chops", + "at_commons", + "at_lookup", + "at_persistence_secondary_server", + "at_utils", + "cron", + "crypto", + "crypton", + "encrypt", + "hive", + "http", + "internet_connection_checker", + "meta", + "mutex", + "path", + "uuid", + "version" + ] + }, + { + "name": "mutex", + "version": "3.1.0", + "dependencies": [] + }, + { + "name": "internet_connection_checker", + "version": "1.0.0+1", + "dependencies": [] + }, + { + "name": "hive", + "version": "2.2.3", + "dependencies": [ + "crypto", + "meta" + ] + }, + { + "name": "encrypt", + "version": "5.0.3", + "dependencies": [ + "args", + "asn1lib", + "clock", + "collection", + "crypto", + "pointycastle" + ] + }, + { + "name": "crypton", + "version": "2.2.1", + "dependencies": [ + "asn1lib", + "pointycastle" + ] + }, + { + "name": "cron", + "version": "0.5.1", + "dependencies": [ + "clock" + ] + }, + { + "name": "at_persistence_secondary_server", + "version": "4.3.5", + "dependencies": [ + "at_commons", + "at_persistence_spec", + "at_utf7", + "at_utils", + "cron", + "crypto", + "hive", + "meta", + "path", + "uuid" + ] + }, + { + "name": "at_base2e15", + "version": "1.0.0", + "dependencies": [] + }, + { + "name": "at_utf7", + "version": "1.0.0", + "dependencies": [] + }, + { + "name": "at_persistence_spec", + "version": "3.1.0", + "dependencies": [] + }, + { + "name": "clock", + "version": "1.1.2", + "dependencies": [] + }, + { + "name": "at_utils", + "version": "3.4.0", + "dependencies": [ + "at_commons", + "chalkdart", + "collection", + "crypto", + "logging", + "yaml" + ] + }, + { + "name": "crypto", + "version": "3.0.7", + "dependencies": [ + "typed_data" + ] + }, + { + "name": "at_commons", + "version": "5.11.0", + "dependencies": [ + "json_annotation", + "meta", + "uuid" + ] + }, + { + "name": "at_auth", + "version": "3.1.0", + "dependencies": [ + "at_chops", + "at_commons", + "at_lookup", + "at_server_status", + "at_utils", + "crypton", + "http", + "meta" + ] + }, + { + "name": "at_lookup", + "version": "3.5.0", + "dependencies": [ + "at_chops", + "at_commons", + "at_utils", + "crypto", + "crypton", + "meta", + "mutex" + ] + }, + { + "name": "at_server_status", + "version": "1.1.1", + "dependencies": [ + "at_lookup" + ] + }, + { + "name": "archive", + "version": "4.0.9", + "dependencies": [ + "path", + "posix" + ] + }, + { + "name": "posix", + "version": "6.5.0", + "dependencies": [ + "ffi", + "meta", + "path" + ] + }, + { + "name": "yaml", + "version": "3.1.3", + "dependencies": [ + "collection", + "source_span", + "string_scanner" + ] + }, + { + "name": "typed_data", + "version": "1.4.0", + "dependencies": [ + "collection" + ] + }, + { + "name": "json_annotation", + "version": "4.12.0", + "dependencies": [ + "meta" + ] + }, + { + "name": "string_scanner", + "version": "1.4.1", + "dependencies": [ + "source_span" + ] + }, + { + "name": "at_chops", + "version": "3.3.0", + "dependencies": [ + "args", + "at_commons", + "at_utils", + "better_cryptography", + "crypto", + "cryptography", + "crypton", + "dart_periphery", + "ecdsa", + "elliptic", + "encrypt", + "ffi", + "meta", + "pointycastle", + "pqcrypto" + ] + }, + { + "name": "pointycastle", + "version": "3.9.1", + "dependencies": [ + "collection", + "convert", + "js" + ] + }, + { + "name": "better_cryptography", + "version": "1.0.0+1", + "dependencies": [ + "collection", + "crypto", + "fixnum", + "js", + "meta", + "typed_data" + ] + }, + { + "name": "pqcrypto", + "version": "0.3.1", + "dependencies": [] + }, + { + "name": "elliptic", + "version": "0.3.12", + "dependencies": [] + }, + { + "name": "ecdsa", + "version": "0.1.2", + "dependencies": [ + "crypto", + "elliptic", + "ninja_asn1" + ] + }, + { + "name": "ninja_asn1", + "version": "2.0.0", + "dependencies": [ + "collection" + ] + }, + { + "name": "fixnum", + "version": "1.1.1", + "dependencies": [] + }, + { + "name": "cryptography", + "version": "2.9.0", + "dependencies": [ + "collection", + "crypto", + "ffi", + "meta", + "typed_data" + ] + }, + { + "name": "js", + "version": "0.6.7", + "dependencies": [ + "meta" + ] + }, + { + "name": "meta", + "version": "1.18.3", + "dependencies": [] + }, + { + "name": "async", + "version": "2.13.1", + "dependencies": [ + "collection", + "meta" + ] + }, + { + "name": "collection", + "version": "1.19.1", + "dependencies": [] + }, + { + "name": "convert", + "version": "3.1.2", + "dependencies": [ + "typed_data" + ] + }, + { + "name": "ffi", + "version": "2.2.0", + "dependencies": [] + }, + { + "name": "http", + "version": "1.6.0", + "dependencies": [ + "async", + "http_parser", + "meta", + "web" + ] + }, + { + "name": "web", + "version": "1.1.1", + "dependencies": [] + }, + { + "name": "http_parser", + "version": "4.1.2", + "dependencies": [ + "collection", + "source_span", + "string_scanner", + "typed_data" + ] + }, + { + "name": "source_span", + "version": "1.10.2", + "dependencies": [ + "collection", + "path", + "term_glyph" + ] + }, + { + "name": "term_glyph", + "version": "1.2.2", + "dependencies": [] + }, + { + "name": "at_onboarding_cli", + "version": "1.15.0", + "dependencies": [ + "args", + "at_auth", + "at_chops", + "at_client", + "at_commons", + "at_lookup", + "at_server_status", + "at_utils", + "chalkdart", + "crypto", + "crypton", + "duration", + "encrypt", + "http", + "image", + "meta", + "path", + "zxing2" + ] + }, + { + "name": "duration", + "version": "4.0.3", + "dependencies": [] + }, + { + "name": "zxing2", + "version": "0.2.4", + "dependencies": [ + "charcode", + "collection", + "fixnum", + "meta" + ] + }, + { + "name": "charcode", + "version": "1.4.0", + "dependencies": [] + }, + { + "name": "asn1lib", + "version": "1.6.5", + "dependencies": [] + }, + { + "name": "chalkdart", + "version": "3.1.0", + "dependencies": [] + }, + { + "name": "uuid", + "version": "4.5.3", + "dependencies": [ + "crypto", + "fixnum" + ] + }, + { + "name": "dart_periphery", + "version": "0.9.20", + "dependencies": [ + "archive", + "async", + "collection", + "ffi", + "meta", + "path", + "process", + "stack_trace" + ] + }, + { + "name": "stack_trace", + "version": "1.12.1", + "dependencies": [ + "path" + ] + }, + { + "name": "process", + "version": "5.0.5", + "dependencies": [ + "file", + "path", + "platform" + ] + }, + { + "name": "platform", + "version": "3.1.6", + "dependencies": [] + }, + { + "name": "file", + "version": "7.0.1", + "dependencies": [ + "meta", + "path" + ] + }, + { + "name": "image", + "version": "4.9.1", + "dependencies": [ + "archive", + "xml" + ] + }, + { + "name": "xml", + "version": "7.0.1", + "dependencies": [ + "collection", + "meta", + "petitparser" + ] + }, + { + "name": "petitparser", + "version": "7.0.2", + "dependencies": [ + "collection", + "meta" + ] + } + ], + "configVersion": 1 +} \ No newline at end of file diff --git a/test/interop/README.md b/test/interop/README.md new file mode 100644 index 0000000..96401ff --- /dev/null +++ b/test/interop/README.md @@ -0,0 +1,35 @@ +# Cross-SDK IV interop test + +`test/interop_test.py` verifies that this Python SDK and the Dart reference +`at_client` interoperate for random-IV / `ivNonce` shared-key encryption, in both +directions: + +- Python `put` shared → Dart `get` shared (Python's random IV read by Dart) +- Dart `put` shared → Python `get` shared (Dart's random IV read by Python) + +This directory holds the Dart helper (`bin/iv_interop.dart` + `pubspec.yaml`) the test +shells out to. + +## Guarded — off by default +`interop_test.py` is discovered by the normal `unittest` run but **skips** unless +`AT_INTEROP=1` and a Dart SDK is on PATH, so it never affects standard/fork CI. + +## Run locally +Prerequisites: Dart SDK; an atServer reachable (e.g. the ephemeral environment); two +onboarded atSigns with `.atKeys` under `$HOME/.atsign/keys`. + +```bash +# EE example: @alpha and @bravo onboarded, keys in /tmp/eehome/.atsign/keys +HOME=/tmp/eehome AT_INTEROP=1 \ + AT_ROOT=vip.ve.atsign.zone:64 AT_ROOT_DOMAIN=vip.ve.atsign.zone \ + python -m unittest discover -s test -p 'interop_test.py' -v +``` + +Env (all optional, EE-friendly defaults): `AT_INTEROP_ATSIGN1` (`@alpha`), +`AT_INTEROP_ATSIGN2` (`@bravo`), `AT_ROOT` (`vip.ve.atsign.zone:64`), `AT_ROOT_DOMAIN` +(host of `AT_ROOT`). + +## CI +A draft opt-in workflow is in `.github/workflows/interop.yml` (manual +`workflow_dispatch`): it starts the ephemeral environment, onboards two atSigns, +installs the SDK, and runs this test with `AT_INTEROP=1`. diff --git a/test/interop/bin/iv_interop.dart b/test/interop/bin/iv_interop.dart new file mode 100644 index 0000000..59bf4d7 --- /dev/null +++ b/test/interop/bin/iv_interop.dart @@ -0,0 +1,83 @@ +// Interop helper using the Dart reference at_client. Put/get self and shared keys +// so a Python peer (at_python fix/put-get-random-iv) can read/write them, proving +// the Python IV/ivNonce behavior is wire-compatible with Dart. +// +// dart run bin/iv_interop.dart --atsign @alpha --root-domain vip.ve.atsign.zone \ +// --op put-shared --key demo --value hi --shared-with @bravo +import 'dart:io'; + +import 'package:at_client/at_client.dart'; +import 'package:at_cli_commons/at_cli_commons.dart'; + +const String ns = 'itest'; + +/// Dart at_client is local-first: put writes locally then syncs to the server, and +/// get of self/own keys reads locally. For cross-SDK interop we must push after a +/// put and pull before a get, so wait until the client is in sync with the server. +Future waitInSync(AtClient atClient, {int timeoutSec = 30}) async { + final sync = atClient.syncService; + for (var i = 0; i < timeoutSec; i++) { + sync.sync(); + try { + if (await sync.isInSync()) return; + } catch (_) {} + await Future.delayed(const Duration(seconds: 1)); + } +} + +Future main(List args) async { + final parser = CLIBase.createArgsParser(namespace: ns) + ..addOption('op', mandatory: true) + ..addOption('key', mandatory: true) + ..addOption('value', defaultsTo: '') + ..addOption('shared-with'); + final cli = await CLIBase.fromCommandLineArgs(args, parser: parser, namespace: ns); + final atClient = cli.atClient; + final me = atClient.getCurrentAtSign()!; + final a = parser.parse(args); + final op = a['op'] as String; + final keyName = a['key'] as String; + final value = a['value'] as String; + final sw = a['shared-with'] as String?; + + switch (op) { + case 'put-self': + final k = AtKey() + ..key = keyName + ..namespace = ns + ..sharedBy = me; + await atClient.put(k, value); + await waitInSync(atClient); // push to server + stdout.writeln('OK'); + break; + case 'get-self': + await waitInSync(atClient); // pull peer's writes + final k = AtKey() + ..key = keyName + ..namespace = ns + ..sharedBy = me; + final r = await atClient.get(k); + stdout.writeln('VALUE:${r.value}'); + break; + case 'put-shared': + final k = AtKey() + ..key = keyName + ..namespace = ns + ..sharedBy = me + ..sharedWith = sw; + await atClient.put(k, value); + await waitInSync(atClient); // push to server + stdout.writeln('OK'); + break; + case 'get-shared': + final k = AtKey() + ..key = keyName + ..namespace = ns + ..sharedBy = sw + ..sharedWith = me; + final r = await atClient.get(k); + stdout.writeln('VALUE:${r.value}'); + break; + } + exit(0); +} diff --git a/test/interop/pubspec.lock b/test/interop/pubspec.lock new file mode 100644 index 0000000..587ea62 --- /dev/null +++ b/test/interop/pubspec.lock @@ -0,0 +1,517 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: "direct main" + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" + url: "https://pub.dev" + source: hosted + version: "1.6.5" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + at_auth: + dependency: transitive + description: + name: at_auth + sha256: "7a4d124122d6b99f6414be8535b6b15d3b8ba81f93597a56fa12460e7f238444" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + at_base2e15: + dependency: transitive + description: + name: at_base2e15 + sha256: "06ee6ffba9b3439f1c41f9bf0c01f579ce0a8b25f42da8c374ba3a14d721937f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + at_chops: + dependency: transitive + description: + name: at_chops + sha256: "4d6ab2cbf9fb87e0d575ffe0cc0622075677b679ea875d5ac39e475ae4be188a" + url: "https://pub.dev" + source: hosted + version: "3.3.0" + at_cli_commons: + dependency: "direct main" + description: + name: at_cli_commons + sha256: "8a651a9baf9c83e689bd80a5e9b4e1b2bb3e2f147475fdefa4cf48a9eb78fd92" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + at_client: + dependency: "direct main" + description: + name: at_client + sha256: "933dd8fc994e36680e011d58e6a2393e4f2e16e8ae4167e49856414db7250768" + url: "https://pub.dev" + source: hosted + version: "3.12.0" + at_commons: + dependency: transitive + description: + name: at_commons + sha256: "3deadcb1f1e0e25550ebc638524a09450f61cfc580db6f50b538f58f787077ac" + url: "https://pub.dev" + source: hosted + version: "5.11.0" + at_lookup: + dependency: transitive + description: + name: at_lookup + sha256: f3e88bfd5144cd5dce845adf3fa89aeb79c97a0e79f8c61aafd4a266ec3b1095 + url: "https://pub.dev" + source: hosted + version: "3.5.0" + at_onboarding_cli: + dependency: transitive + description: + name: at_onboarding_cli + sha256: "40b9b40ff88bbfba50412bbc6fe4d5991958e6ccab9a1f434bba0eaa9af115b9" + url: "https://pub.dev" + source: hosted + version: "1.15.0" + at_persistence_secondary_server: + dependency: transitive + description: + name: at_persistence_secondary_server + sha256: "869949a4e76b7c8ad8f5a5aa4698f7df11a177964c9e3261b96c1dd0d5ee03e3" + url: "https://pub.dev" + source: hosted + version: "4.3.5" + at_persistence_spec: + dependency: transitive + description: + name: at_persistence_spec + sha256: "2b8a414f53ae2368023862cda0b6c2ccef3a3876acc355ea064654df434aedbd" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + at_server_status: + dependency: transitive + description: + name: at_server_status + sha256: bbce2d6226b5a381a5462ba99afc6e6d677043263a4eb659a731d0f51465bc54 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + at_utf7: + dependency: transitive + description: + name: at_utf7 + sha256: c88e964e307bfe0e53e0048cff1ebf5ab60e23ceb4273f1ca664e724a9a5c5c9 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + at_utils: + dependency: transitive + description: + name: at_utils + sha256: e974616a816e9f6f3b186f33efea823f345d193b6ea9b42f08d28f35848c4de1 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + better_cryptography: + dependency: transitive + description: + name: better_cryptography + sha256: "67573ef169a3584710038f92e81b75c7790933af782a83ba8f71893496493de3" + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" + chalkdart: + dependency: transitive + description: + name: chalkdart + sha256: "7dcf37e0b3d8dcec8c4ae0420d85aad9b3167d6a759601fd5b0f2b1958746581" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cron: + dependency: transitive + description: + name: cron + sha256: d98aa8cdad0cccdb6b098e6a1fb89339c180d8a229145fa4cd8c6fc538f0e35f + url: "https://pub.dev" + source: hosted + version: "0.5.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: "3eda3029d34ec9095a27a198ac9785630fe525c0eb6a49f3d575272f8e792ef0" + url: "https://pub.dev" + source: hosted + version: "2.9.0" + crypton: + dependency: transitive + description: + name: crypton + sha256: "17b6631fbf89e389d421b46629132287ed37d601b2ad1357445826ab85022271" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + dart_periphery: + dependency: transitive + description: + name: dart_periphery + sha256: "1c933dae787d65e6e66ee89db4a58ed74dc9213b83cea10c344585a256ca498a" + url: "https://pub.dev" + source: hosted + version: "0.9.20" + duration: + dependency: transitive + description: + name: duration + sha256: "13e5d20723c9c1dde8fb318cf86716d10ce294734e81e44ae1a817f3ae714501" + url: "https://pub.dev" + source: hosted + version: "4.0.3" + ecdsa: + dependency: transitive + description: + name: ecdsa + sha256: "13b4e01ac140575cf88ef5e5268f92b8dd0a67a26c60a1b74e67d146068dc246" + url: "https://pub.dev" + source: hosted + version: "0.1.2" + elliptic: + dependency: transitive + description: + name: elliptic + sha256: "67931d408faa353bdebac9f7a1df0c3f6f828f4e8439cdf084573cd1601a2f4b" + url: "https://pub.dev" + source: hosted + version: "0.3.12" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52" + url: "https://pub.dev" + source: hosted + version: "4.9.1" + internet_connection_checker: + dependency: transitive + description: + name: internet_connection_checker + sha256: "1c683e63e89c9ac66a40748b1b20889fd9804980da732bf2b58d6d5456c8e876" + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + meta: + dependency: transitive + description: + name: meta + sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d + url: "https://pub.dev" + source: hosted + version: "1.18.3" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + ninja_asn1: + dependency: transitive + description: + name: ninja_asn1 + sha256: b0f04877243fda51c475ec2bcaadb55a92759baee9f02888124c60775760ccf7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + pqcrypto: + dependency: transitive + description: + name: pqcrypto + sha256: fa8bd7eac5ccb4389f4003715a79b1b601ca9439cb81d1174a1e06da75306b39 + url: "https://pub.dev" + source: hosted + version: "0.3.1" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + version: + dependency: transitive + description: + name: version + sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xml: + dependency: transitive + description: + name: xml + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + zxing2: + dependency: transitive + description: + name: zxing2 + sha256: "2677c49a3b9ca9457cb1d294fd4bd5041cac6aab8cdb07b216ba4e98945c684f" + url: "https://pub.dev" + source: hosted + version: "0.2.4" +sdks: + dart: ">=3.11.0 <4.0.0" diff --git a/test/interop/pubspec.yaml b/test/interop/pubspec.yaml new file mode 100644 index 0000000..c2d8410 --- /dev/null +++ b/test/interop/pubspec.yaml @@ -0,0 +1,11 @@ +name: at_python_interop +description: Dart helper for cross-SDK IV interop tests against the Python SDK. +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + at_client: ^3.11.0 + at_cli_commons: ^2.0.0 + args: ^2.4.0 diff --git a/test/interop_test.py b/test/interop_test.py new file mode 100644 index 0000000..6dccf0e --- /dev/null +++ b/test/interop_test.py @@ -0,0 +1,102 @@ +""" +Cross-SDK IV interop test: this Python SDK <-> the Dart reference at_client. + +Proves random-IV / ivNonce shared-key encryption interoperates in both directions. +GUARDED: skipped unless AT_INTEROP=1 (and a Dart SDK is on PATH), so it never affects +the normal `unittest discover` run or fork CI. + +Prerequisites when AT_INTEROP=1: + - Dart SDK on PATH + - an atServer reachable (e.g. the ephemeral environment) at AT_ROOT + - two onboarded atSigns with .atKeys under $HOME/.atsign/keys + +Env (all optional, with EE-friendly defaults): + AT_INTEROP=1 enable this test + AT_INTEROP_ATSIGN1=@alpha atSign A + AT_INTEROP_ATSIGN2=@bravo atSign B + AT_ROOT=vip.ve.atsign.zone:64 root for the Python client + AT_ROOT_DOMAIN=vip.ve.atsign.zone root domain for the Dart helper (host only) +""" +import os +import shutil +import subprocess +import unittest + +NS = "itest" +HERE = os.path.dirname(os.path.abspath(__file__)) +DART_DIR = os.path.join(HERE, "interop") + +ATSIGN1 = os.environ.get("AT_INTEROP_ATSIGN1", "@alpha") +ATSIGN2 = os.environ.get("AT_INTEROP_ATSIGN2", "@bravo") +ROOT = os.environ.get("AT_ROOT", "vip.ve.atsign.zone:64") +ROOT_DOMAIN = os.environ.get("AT_ROOT_DOMAIN", ROOT.split(":")[0]) + +_enabled = os.environ.get("AT_INTEROP") == "1" and shutil.which("dart") is not None + + +@unittest.skipUnless(_enabled, "interop disabled (set AT_INTEROP=1 and install Dart)") +class InteropTest(unittest.TestCase): + + _n = 0 + + @classmethod + def setUpClass(cls): + subprocess.run(["dart", "pub", "get"], cwd=DART_DIR, check=True, + capture_output=True) + + def _key(self, prefix): + InteropTest._n += 1 + return f"{prefix}{os.getpid()}x{InteropTest._n}" + + # ---- Python side (the library under test) ---- + def _py(self, atsign, op, key, value=None, shared_with=None): + from at_client import AtClient + from at_client.common import AtSign + from at_client.common.keys import SharedKey + from at_client.connections import Address + + client = AtClient(AtSign(atsign), root_address=Address.from_string(ROOT)) + if op == "put-shared": + k = SharedKey(key, AtSign(atsign), AtSign(shared_with)) + k.set_namespace(NS) + client.put(k, value) + return None + elif op == "get-shared": + k = SharedKey(key, AtSign(shared_with), AtSign(atsign)) + k.set_namespace(NS) + return client.get(k) + raise ValueError(op) + + # ---- Dart side (reference at_client) ---- + def _dart(self, atsign, op, key, value=None, shared_with=None): + cmd = ["dart", "run", "bin/iv_interop.dart", + "--atsign", atsign, "--root-domain", ROOT_DOMAIN, "--op", op, "--key", key] + if value is not None: + cmd += ["--value", value] + if shared_with is not None: + cmd += ["--shared-with", shared_with] + out = subprocess.run(cmd, cwd=DART_DIR, check=True, capture_output=True, text=True) + for line in out.stdout.splitlines(): + if line.startswith("VALUE:"): + return line[len("VALUE:"):] + return None + + def test_python_put_shared_dart_get_shared(self): + """Dart reads a shared key Python wrote (Python's random IV -> Dart).""" + key = self._key("pd") + val = f"PY2DART_{key}" + self._py(ATSIGN1, "put-shared", key, value=val, shared_with=ATSIGN2) + got = self._dart(ATSIGN2, "get-shared", key, shared_with=ATSIGN1) + self.assertEqual(got, val) + + def test_dart_put_shared_python_get_shared(self): + """Python reads a shared key Dart wrote (Dart's random IV -> Python).""" + key = self._key("dp") + val = f"DART2PY_{key}" + self._dart(ATSIGN2, "put-shared", key, value=val, shared_with=ATSIGN1) + got = self._py(ATSIGN1, "get-shared", key, shared_with=ATSIGN2) + self.assertEqual(got, val) + + +if __name__ == "__main__": + unittest.main() From d51235888ce9920692a4201d285d2a5384183e74 Mon Sep 17 00:00:00 2001 From: Colin Constable Date: Thu, 2 Jul 2026 17:25:37 -0700 Subject: [PATCH 5/7] test(interop): clear Dart local storage before run; note EE-recreate gotcha Defensive rm of $HOME/.atsign/storage before the interop run (harmless on a fresh runner; avoids stale-key decrypt failures when re-running against a recreated EE). --- .github/workflows/interop.yml | 3 + test/interop/.dart_tool/package_graph.json | 544 ++++++++++----------- test/interop/README.md | 4 + 3 files changed, 279 insertions(+), 272 deletions(-) diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml index 3ab7e8c..f6a2c2b 100644 --- a/.github/workflows/interop.yml +++ b/.github/workflows/interop.yml @@ -77,6 +77,9 @@ jobs: - name: Run interop test run: | + # Start from clean Dart local storage (keeps .atKeys). Harmless on a fresh + # runner; prevents stale-key errors when re-running against a recreated EE. + rm -rf /tmp/eehome/.atsign/storage HOME=/tmp/eehome AT_INTEROP=1 \ AT_ROOT=vip.ve.atsign.zone:64 AT_ROOT_DOMAIN=vip.ve.atsign.zone \ python -m unittest discover -s test -p 'interop_test.py' -v diff --git a/test/interop/.dart_tool/package_graph.json b/test/interop/.dart_tool/package_graph.json index 6b04949..8567058 100644 --- a/test/interop/.dart_tool/package_graph.json +++ b/test/interop/.dart_tool/package_graph.json @@ -13,6 +13,11 @@ ], "devDependencies": [] }, + { + "name": "args", + "version": "2.7.0", + "dependencies": [] + }, { "name": "at_cli_commons", "version": "2.2.0", @@ -28,26 +33,6 @@ "version" ] }, - { - "name": "version", - "version": "3.0.2", - "dependencies": [] - }, - { - "name": "args", - "version": "2.7.0", - "dependencies": [] - }, - { - "name": "path", - "version": "1.9.1", - "dependencies": [] - }, - { - "name": "logging", - "version": "1.3.0", - "dependencies": [] - }, { "name": "at_client", "version": "3.12.0", @@ -76,111 +61,92 @@ ] }, { - "name": "mutex", - "version": "3.1.0", + "name": "path", + "version": "1.9.1", "dependencies": [] }, { - "name": "internet_connection_checker", - "version": "1.0.0+1", + "name": "meta", + "version": "1.18.3", "dependencies": [] }, { - "name": "hive", - "version": "2.2.3", - "dependencies": [ - "crypto", - "meta" - ] + "name": "logging", + "version": "1.3.0", + "dependencies": [] }, { - "name": "encrypt", - "version": "5.0.3", - "dependencies": [ - "args", - "asn1lib", - "clock", - "collection", - "crypto", - "pointycastle" - ] + "name": "version", + "version": "3.0.2", + "dependencies": [] }, { - "name": "crypton", - "version": "2.2.1", - "dependencies": [ - "asn1lib", - "pointycastle" - ] + "name": "chalkdart", + "version": "3.1.0", + "dependencies": [] }, { - "name": "cron", - "version": "0.5.1", + "name": "at_utils", + "version": "3.4.0", "dependencies": [ - "clock" + "at_commons", + "chalkdart", + "collection", + "crypto", + "logging", + "yaml" ] }, { - "name": "at_persistence_secondary_server", - "version": "4.3.5", + "name": "at_onboarding_cli", + "version": "1.15.0", "dependencies": [ + "args", + "at_auth", + "at_chops", + "at_client", "at_commons", - "at_persistence_spec", - "at_utf7", + "at_lookup", + "at_server_status", "at_utils", - "cron", + "chalkdart", "crypto", - "hive", + "crypton", + "duration", + "encrypt", + "http", + "image", "meta", "path", - "uuid" + "zxing2" ] }, { - "name": "at_base2e15", - "version": "1.0.0", - "dependencies": [] - }, - { - "name": "at_utf7", - "version": "1.0.0", - "dependencies": [] - }, - { - "name": "at_persistence_spec", + "name": "mutex", "version": "3.1.0", "dependencies": [] }, { - "name": "clock", - "version": "1.1.2", - "dependencies": [] - }, - { - "name": "at_utils", - "version": "3.4.0", + "name": "hive", + "version": "2.2.3", "dependencies": [ - "at_commons", - "chalkdart", - "collection", "crypto", - "logging", - "yaml" - ] - }, - { - "name": "crypto", - "version": "3.0.7", - "dependencies": [ - "typed_data" + "meta" ] }, { - "name": "at_commons", - "version": "5.11.0", + "name": "at_persistence_secondary_server", + "version": "4.3.5", "dependencies": [ - "json_annotation", + "at_commons", + "at_persistence_spec", + "at_utf7", + "at_utils", + "cron", + "crypto", + "hive", "meta", + "path", "uuid" ] }, @@ -211,60 +177,6 @@ "mutex" ] }, - { - "name": "at_server_status", - "version": "1.1.1", - "dependencies": [ - "at_lookup" - ] - }, - { - "name": "archive", - "version": "4.0.9", - "dependencies": [ - "path", - "posix" - ] - }, - { - "name": "posix", - "version": "6.5.0", - "dependencies": [ - "ffi", - "meta", - "path" - ] - }, - { - "name": "yaml", - "version": "3.1.3", - "dependencies": [ - "collection", - "source_span", - "string_scanner" - ] - }, - { - "name": "typed_data", - "version": "1.4.0", - "dependencies": [ - "collection" - ] - }, - { - "name": "json_annotation", - "version": "4.12.0", - "dependencies": [ - "meta" - ] - }, - { - "name": "string_scanner", - "version": "1.4.1", - "dependencies": [ - "source_span" - ] - }, { "name": "at_chops", "version": "3.3.0", @@ -287,113 +199,221 @@ ] }, { - "name": "pointycastle", - "version": "3.9.1", + "name": "at_commons", + "version": "5.11.0", "dependencies": [ - "collection", - "convert", - "js" + "json_annotation", + "meta", + "uuid" ] }, { - "name": "better_cryptography", - "version": "1.0.0+1", + "name": "at_base2e15", + "version": "1.0.0", + "dependencies": [] + }, + { + "name": "async", + "version": "2.13.1", "dependencies": [ "collection", - "crypto", - "fixnum", - "js", - "meta", - "typed_data" + "meta" ] }, { - "name": "pqcrypto", - "version": "0.3.1", + "name": "internet_connection_checker", + "version": "1.0.0+1", "dependencies": [] }, { - "name": "elliptic", - "version": "0.3.12", - "dependencies": [] + "name": "http", + "version": "1.6.0", + "dependencies": [ + "async", + "http_parser", + "meta", + "web" + ] }, { - "name": "ecdsa", - "version": "0.1.2", + "name": "archive", + "version": "4.0.9", "dependencies": [ - "crypto", - "elliptic", - "ninja_asn1" + "path", + "posix" ] }, { - "name": "ninja_asn1", - "version": "2.0.0", + "name": "uuid", + "version": "4.5.3", "dependencies": [ - "collection" + "crypto", + "fixnum" ] }, { - "name": "fixnum", - "version": "1.1.1", - "dependencies": [] + "name": "cron", + "version": "0.5.1", + "dependencies": [ + "clock" + ] }, { - "name": "cryptography", - "version": "2.9.0", + "name": "encrypt", + "version": "5.0.3", "dependencies": [ + "args", + "asn1lib", + "clock", "collection", "crypto", - "ffi", - "meta", + "pointycastle" + ] + }, + { + "name": "crypton", + "version": "2.2.1", + "dependencies": [ + "asn1lib", + "pointycastle" + ] + }, + { + "name": "crypto", + "version": "3.0.7", + "dependencies": [ "typed_data" ] }, { - "name": "js", - "version": "0.6.7", + "name": "collection", + "version": "1.19.1", + "dependencies": [] + }, + { + "name": "yaml", + "version": "3.1.3", "dependencies": [ - "meta" + "collection", + "source_span", + "string_scanner" ] }, { - "name": "meta", - "version": "1.18.3", + "name": "duration", + "version": "4.0.3", "dependencies": [] }, { - "name": "async", - "version": "2.13.1", + "name": "at_server_status", + "version": "1.1.1", + "dependencies": [ + "at_lookup" + ] + }, + { + "name": "zxing2", + "version": "0.2.4", "dependencies": [ + "charcode", "collection", + "fixnum", "meta" ] }, { - "name": "collection", - "version": "1.19.1", + "name": "image", + "version": "4.9.1", + "dependencies": [ + "archive", + "xml" + ] + }, + { + "name": "at_persistence_spec", + "version": "3.1.0", "dependencies": [] }, { - "name": "convert", - "version": "3.1.2", + "name": "at_utf7", + "version": "1.0.0", + "dependencies": [] + }, + { + "name": "pqcrypto", + "version": "0.3.1", + "dependencies": [] + }, + { + "name": "ffi", + "version": "2.2.0", + "dependencies": [] + }, + { + "name": "better_cryptography", + "version": "1.0.0+1", "dependencies": [ + "collection", + "crypto", + "fixnum", + "js", + "meta", "typed_data" ] }, { - "name": "ffi", - "version": "2.2.0", + "name": "cryptography", + "version": "2.9.0", + "dependencies": [ + "collection", + "crypto", + "ffi", + "meta", + "typed_data" + ] + }, + { + "name": "pointycastle", + "version": "3.9.1", + "dependencies": [ + "collection", + "convert", + "js" + ] + }, + { + "name": "elliptic", + "version": "0.3.12", "dependencies": [] }, { - "name": "http", - "version": "1.6.0", + "name": "dart_periphery", + "version": "0.9.20", "dependencies": [ + "archive", "async", - "http_parser", + "collection", + "ffi", "meta", - "web" + "path", + "process", + "stack_trace" + ] + }, + { + "name": "ecdsa", + "version": "0.1.2", + "dependencies": [ + "crypto", + "elliptic", + "ninja_asn1" + ] + }, + { + "name": "json_annotation", + "version": "4.12.0", + "dependencies": [ + "meta" ] }, { @@ -412,56 +432,50 @@ ] }, { - "name": "source_span", - "version": "1.10.2", + "name": "posix", + "version": "6.5.0", "dependencies": [ - "collection", - "path", - "term_glyph" + "ffi", + "meta", + "path" ] }, { - "name": "term_glyph", - "version": "1.2.2", + "name": "fixnum", + "version": "1.1.1", "dependencies": [] }, { - "name": "at_onboarding_cli", - "version": "1.15.0", + "name": "clock", + "version": "1.1.2", + "dependencies": [] + }, + { + "name": "asn1lib", + "version": "1.6.5", + "dependencies": [] + }, + { + "name": "typed_data", + "version": "1.4.0", "dependencies": [ - "args", - "at_auth", - "at_chops", - "at_client", - "at_commons", - "at_lookup", - "at_server_status", - "at_utils", - "chalkdart", - "crypto", - "crypton", - "duration", - "encrypt", - "http", - "image", - "meta", - "path", - "zxing2" + "collection" ] }, { - "name": "duration", - "version": "4.0.3", - "dependencies": [] + "name": "string_scanner", + "version": "1.4.1", + "dependencies": [ + "source_span" + ] }, { - "name": "zxing2", - "version": "0.2.4", + "name": "source_span", + "version": "1.10.2", "dependencies": [ - "charcode", "collection", - "fixnum", - "meta" + "path", + "term_glyph" ] }, { @@ -470,35 +484,26 @@ "dependencies": [] }, { - "name": "asn1lib", - "version": "1.6.5", - "dependencies": [] - }, - { - "name": "chalkdart", - "version": "3.1.0", - "dependencies": [] + "name": "xml", + "version": "7.0.1", + "dependencies": [ + "collection", + "meta", + "petitparser" + ] }, { - "name": "uuid", - "version": "4.5.3", + "name": "js", + "version": "0.6.7", "dependencies": [ - "crypto", - "fixnum" + "meta" ] }, { - "name": "dart_periphery", - "version": "0.9.20", + "name": "convert", + "version": "3.1.2", "dependencies": [ - "archive", - "async", - "collection", - "ffi", - "meta", - "path", - "process", - "stack_trace" + "typed_data" ] }, { @@ -518,41 +523,36 @@ ] }, { - "name": "platform", - "version": "3.1.6", - "dependencies": [] - }, - { - "name": "file", - "version": "7.0.1", + "name": "ninja_asn1", + "version": "2.0.0", "dependencies": [ - "meta", - "path" + "collection" ] }, { - "name": "image", - "version": "4.9.1", - "dependencies": [ - "archive", - "xml" - ] + "name": "term_glyph", + "version": "1.2.2", + "dependencies": [] }, { - "name": "xml", - "version": "7.0.1", + "name": "petitparser", + "version": "7.0.2", "dependencies": [ "collection", - "meta", - "petitparser" + "meta" ] }, { - "name": "petitparser", - "version": "7.0.2", + "name": "platform", + "version": "3.1.6", + "dependencies": [] + }, + { + "name": "file", + "version": "7.0.1", "dependencies": [ - "collection", - "meta" + "meta", + "path" ] } ], diff --git a/test/interop/README.md b/test/interop/README.md index 96401ff..f9fcd6b 100644 --- a/test/interop/README.md +++ b/test/interop/README.md @@ -29,6 +29,10 @@ Env (all optional, EE-friendly defaults): `AT_INTEROP_ATSIGN1` (`@alpha`), `AT_INTEROP_ATSIGN2` (`@bravo`), `AT_ROOT` (`vip.ve.atsign.zone:64`), `AT_ROOT_DOMAIN` (host of `AT_ROOT`). +> Re-running against a **recreated** EE? Clear the Dart client's local storage first +> (`rm -rf $HOME/.atsign/storage`) — it caches keys from the previous atServer and will +> otherwise fail to decrypt after re-onboarding. A fresh CI runner never hits this. + ## CI A draft opt-in workflow is in `.github/workflows/interop.yml` (manual `workflow_dispatch`): it starts the ephemeral environment, onboards two atSigns, From 77617ffc0e99535a9e830f7b108904e8d4b97a56 Mon Sep 17 00:00:00 2001 From: Colin Constable Date: Thu, 2 Jul 2026 17:31:27 -0700 Subject: [PATCH 6/7] ci(interop): TEMP push trigger to validate the interop workflow pre-merge --- .github/workflows/interop.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml index f6a2c2b..da75549 100644 --- a/.github/workflows/interop.yml +++ b/.github/workflows/interop.yml @@ -6,6 +6,8 @@ name: interop (Python <-> Dart) on: workflow_dispatch: + push: + branches: [ fix/put-get-random-iv ] # TEMP: to trigger a validation run pre-merge permissions: contents: read From 1ddf934c48ab9e56de64573791a7c6396a56d579 Mon Sep 17 00:00:00 2001 From: Colin Constable Date: Thu, 2 Jul 2026 17:33:33 -0700 Subject: [PATCH 7/7] ci(interop): revert temp push trigger, back to workflow_dispatch only [skip ci] --- .github/workflows/interop.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml index da75549..f6a2c2b 100644 --- a/.github/workflows/interop.yml +++ b/.github/workflows/interop.yml @@ -6,8 +6,6 @@ name: interop (Python <-> Dart) on: workflow_dispatch: - push: - branches: [ fix/put-get-random-iv ] # TEMP: to trigger a validation run pre-merge permissions: contents: read