From c7973dac2c2e9ff13f74abe238cd06a1de7d178a Mon Sep 17 00:00:00 2001 From: HumbleTurtle Date: Tue, 5 May 2026 03:55:25 -0300 Subject: [PATCH] Sync lane-bound relay crypto in CLI runtime --- resources/scripts/napseer_spake2.py | 6 +- tests/test_gateway_vault_setup_payload.py | 75 +++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 tests/test_gateway_vault_setup_payload.py diff --git a/resources/scripts/napseer_spake2.py b/resources/scripts/napseer_spake2.py index f21ae72..f933b20 100755 --- a/resources/scripts/napseer_spake2.py +++ b/resources/scripts/napseer_spake2.py @@ -194,18 +194,20 @@ def spake2_gateway_finish(secret, spake2_context, browser_message): def relay_key_from_secret(secret, spake2_context, direction): context = canonical_json(spake2_context) + relay_lane = str(spake2_context.get("relay_lane") or spake2_context.get("relay_socket") or "terminal") salt = hashlib.sha256(context.encode("utf-8")).digest() - return hkdf_sha256(secret, salt, f"napseer-relay-aes-gcm:{direction}") + return hkdf_sha256(secret, salt, f"napseer-relay-aes-gcm:{relay_lane}:{direction}") def relay_context_hash(spake2_context): return base64.b64encode(hashlib.sha256(canonical_json(spake2_context).encode("utf-8")).digest()).decode("ascii") -def relay_aad(session_id, context_hash, direction, seq): +def relay_aad(session_id, context_hash, relay_lane, direction, seq): return canonical_json({ "context_hash": context_hash, "direction": direction, + "relay_lane": relay_lane, "seq": seq, "session_id": session_id, }).encode("utf-8") diff --git a/tests/test_gateway_vault_setup_payload.py b/tests/test_gateway_vault_setup_payload.py new file mode 100644 index 0000000..c7dcaba --- /dev/null +++ b/tests/test_gateway_vault_setup_payload.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Smoke-test gateway-owned project vault setup payload generation.""" + +import base64 +import importlib.util +import pathlib +import sys + + +def load_module(): + script_path = pathlib.Path(__file__).resolve().parents[1] / "resources" / "scripts" / "napseer_mcp_server.py" + sys.path.insert(0, str(script_path.parent)) + spec = importlib.util.spec_from_file_location("napseer_mcp_server", script_path) + module = importlib.util.module_from_spec(spec) + assert spec and spec.loader + spec.loader.exec_module(module) + return module + + +def run(): + mod = load_module() + writes = [] + + mod.AUTH = {"agent_id": "gateway-agent-1"} + mod.VAULT_KEY = bytes(range(32)) + mod.VAULT_SECRETS = {} + mod.persist_vault_secrets = lambda: writes.append(dict(mod.VAULT_SECRETS)) + + payload = mod.gateway_generate_project_vault_setup_payload("project-1", "setup-1") + assert mod.VAULT_KDF_ITERATIONS == 600000 + assert writes, "payload generation should persist local encrypted vault state" + assert set(item["secret_kind"] for item in payload["project_secrets"]) == {"chat", "tabs", "gateway"} + assert payload["wrapped_master_secret"]["credential_id"] == "gateway-vault:gateway-agent-1" + assert payload["wrapped_master_secret"]["kdf_params"]["iterations"] == 600000 + + for item in payload["project_secrets"]: + envelope = item["encrypted_secret_envelope"] + assert envelope["schema_version"] == 1 + assert envelope["alg"] == "AES-GCM-256" + assert envelope["key_id"] == "vault-master:project-1:v1" + assert "plaintext" not in envelope + assert "content" not in envelope + assert len(base64.b64decode(envelope["nonce_b64"])) == 12 + assert len(base64.b64decode(envelope["ciphertext_b64"])) == envelope["payload_size_bytes"] + assert envelope["payload_size_bytes"] > 16 + + stored = mod.VAULT_SECRETS["project_vaults"]["project-1"] + assert stored["setup_request_id"] == "setup-1" + assert len(base64.b64decode(stored["vault_master_secret_b64"])) == 32 + assert set(stored["project_secrets"]) == {"chat", "tabs", "gateway"} + + writes.clear() + calls = [] + mod.DEFAULT_PROJECT_ID = "project-1" + mod.require_unlocked = lambda operation="operation": None + mod.request_project_write = lambda method, path, body, project_id, purpose, scope_type="project": calls.append( + (method, path, body, project_id, purpose, scope_type) + ) or { + "id": "secret-version-2", + "version": 2, + "secret_kind": "chat", + } + rotated = mod.gateway_rotate_project_vault_secret({"project_id": "project-1", "secret_kind": "chat"}) + assert rotated["version"] == 2 + assert calls[0][0] == "POST" + assert calls[0][1] == "/v1/projects/project-1/vault/secrets/chat/rotate" + assert calls[0][5] == "encryption" + assert calls[0][2]["encrypted_secret_envelope"]["key_id"] == "vault-master:project-1:v1" + assert mod.VAULT_SECRETS["project_vaults"]["project-1"]["project_secrets"]["chat"]["version"] == 2 + + print("ok: gateway vault setup payload smoke passed") + + +if __name__ == "__main__": + run()