From 5cda14d579c3e13d20ddef128a8e9eb412404261 Mon Sep 17 00:00:00 2001 From: "Morten V. Christiansen" Date: Tue, 3 Mar 2026 14:42:27 +0100 Subject: [PATCH] added crypto functions --- .../zenroom_service_client.cpython-313.pyc | Bin 7936 -> 19850 bytes ca_core/crypto/zenroom_service_client.py | 341 +++++++++++++++++- ...est_zenroom_service_client.cpython-313.pyc | Bin 6790 -> 13574 bytes tests/integration/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 141 bytes .../test_zenroom_live.cpython-313.pyc | Bin 0 -> 4184 bytes ...service_client_integration.cpython-313.pyc | Bin 1939 -> 2237 bytes ...test_zenroom_service_client_integration.py | 31 +- tests/test_zenroom_service_client.py | 197 +++++++++- 9 files changed, 517 insertions(+), 52 deletions(-) create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/__pycache__/__init__.cpython-313.pyc create mode 100644 tests/integration/__pycache__/test_zenroom_live.cpython-313.pyc diff --git a/ca_core/crypto/__pycache__/zenroom_service_client.cpython-313.pyc b/ca_core/crypto/__pycache__/zenroom_service_client.cpython-313.pyc index d90133f9687528b82c829e56b8ff69b2ad1ab1a7..414e9689352762b9928f28016a2858c9f6d1b5d2 100644 GIT binary patch literal 19850 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 delta 2227 zcmZWqT}%{L6rS0c{fFIUfmIfi%ksN|3#srRHQ0(oO!21-t42^dE*BWxomub9qKmO= zY8u-<7<+3e6WRwuLz>v9+Lxw%X)rw0Ol+ckYZ@D(roQx{=gfl8^dx)s-ZSTX z=iGD8Irqa`;^n^3^YZe505o~$OD$7*BeYpOextSX01-9|2|ib{K7?DQdAjuwwnr782xCN7B0?2*-=WsA)?8ZM$YEOM|wF zXNS0uAl)BT^=!EflFP!wQ1trg4^Gcj-SN!JbMG#PQrD!%!CL;L>MC5wWc8nYj>n_u z(LYkm)75O@K5#_M7D@t0#0e0wlPDKwHl*QtY)~BM4`XlFVlr%Rt~jo%Ihw&MTq6&^ zva^S`cU1Gp;YRj%O*{zGSFjb{y5mp2&7>xK>MhTgPmmn$UfaIM);F)gp+ z_|{~)djpPpG|CrRYur%xX*WL`tmA*R`Z#HFC)ZK^|3_5dhivxcLTcD zL9cVAy{pjvQE01UGfEj&aoW=)B+J!rA#wYtPMK;^qUvOzfA1hULyLJ;W29he`V_II zDH$G6MzAluBx@8hIYZ^q;Gugs~qXJ5InyR@@$}R;0Bh#8ma@3kO5ZNj)-9#9Iapjpd1IHbQ zV`50DR4UN{(!!~%mf0wT07=`rm>7DtNVG|khfv`V$pE8}$l26d{zOPS-}ljitr zY#^7nim#HncD~sr-qBDKgcdDiATHWV_*0VST0{G#Aj7fEMv-9|+p;+W zUYID=aqesEiQ))=QQlq)&B(B8 z7(N{gXD!1q?94|eqjnj*J(iX!6-6o23^hW?a8{IFPs|33mKaFUt=T2=XM;a(3%V=k z%oPFt=9}ith5q^TU!|Pwy^;Hk_s#ol-<^DcUsvM%_x;IT9iS(z2=KQcFDUmK7mj@s zdWqYTBlM9BvM%_uL-3GfwNoiZ&%zyBYzKcj)V9Me8!kYz(Qq2+}~6#a7@F9i5|>D%uTE1wC-9pC=~Prnez diff --git a/ca_core/crypto/zenroom_service_client.py b/ca_core/crypto/zenroom_service_client.py index 1ad8473..09300e8 100644 --- a/ca_core/crypto/zenroom_service_client.py +++ b/ca_core/crypto/zenroom_service_client.py @@ -1,7 +1,7 @@ import json import urllib.request import urllib.error -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Tuple class ZenroomServiceError(RuntimeError): @@ -80,8 +80,11 @@ class ZenroomServiceClient: return self._request_json("POST", path, payload) def _post_data(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]: + # Per your rule: if "keys" is empty, omit it entirely. + # All your services in this round only need {"data": ...} res = self._post(path, {"data": data}) + # RESTroom convention: on failure you get zenroom_errors and/or exception if "zenroom_errors" in res or "exception" in res: exc = res.get("exception", "") ze = res.get("zenroom_errors") @@ -101,20 +104,52 @@ class ZenroomServiceClient: raise ValueError(f"{name} cannot be empty") return v - def generate_keypair(self, my_name: str) -> Dict[str, str]: - """Generate an ECDH keypair using the RESTroom contract: + @staticmethod + def _require_dict(name: str, value: Any) -> Dict[str, Any]: + if not isinstance(value, dict): + raise TypeError(f"{name} must be a dict") + return value - POST /api/Generate-a-keypair,-reading-identity-from-data - body: { "data": { "myName": "" } } + @staticmethod + def _require_keys(d: Dict[str, Any], *, required: Tuple[str, ...], ctx: str) -> None: + missing = [k for k in required if k not in d] + if missing: + raise ZenroomServiceError(f"Missing {missing} in {ctx}: {d!r}") - Observed response from your service: - { "": { "keyring": { "ecdh": "" } } } + def _pick_owner_block(self, res: Dict[str, Any], my_name: str, ctx: str) -> Dict[str, Any]: + """ + Zenroom often returns: { "": { ... } } + Prefer res[my_name] when present, else accept single-entry dict. + """ + if my_name in res: + owner = res[my_name] + elif len(res) == 1: + owner = next(iter(res.values())) + else: + raise ZenroomServiceError(f"Ambiguous {ctx} response (no key '{my_name}', len={len(res)}): {res!r}") - Some variants also include: - "ecdh_public_key": "" + if not isinstance(owner, dict): + raise ZenroomServiceError(f"Invalid {ctx} response structure: {res!r}") + return owner - This method returns: - { "private_key": "...", "public_key": "..." } (public_key only if present) + # ------------------------------------------------------------------------- + # Service 1: Generate-a-keypair,-reading-identity-from-data + # ------------------------------------------------------------------------- + def generate_keypair(self, my_name: str) -> Dict[str, Any]: + """ + POST Generate-a-keypair,-reading-identity-from-data + body: {"data": {"myName": ""}} + + Observed response: + { "": { "keyring": { "ecdh": "" } } } + + Return (normalized, plus backward-compatible fields): + { + "my_name": "", + "keyring": {"ecdh": ""}, + "private_key": "", + # "public_key": "" only if the service variant returned it + } """ my_name = self._require_non_empty_str("my_name", my_name) @@ -123,13 +158,7 @@ class ZenroomServiceClient: {"myName": my_name}, ) - if not res: - raise ZenroomServiceError("Empty keypair response") - - # Zenroom typically returns { "": { ... } } - owner = next(iter(res.values())) - if not isinstance(owner, dict): - raise ZenroomServiceError(f"Invalid keypair response structure: {res!r}") + owner = self._pick_owner_block(res, my_name, "keypair") keyring = owner.get("keyring") if not isinstance(keyring, dict): @@ -139,10 +168,284 @@ class ZenroomServiceClient: if not isinstance(private_key, str) or not private_key.strip(): raise ZenroomServiceError(f"Invalid keypair response (missing keyring.ecdh): {res!r}") - out: Dict[str, str] = {"private_key": private_key} + out: Dict[str, Any] = { + "my_name": my_name, + "keyring": keyring, + "private_key": private_key, # convenience alias + } + # Some variants might include this (but your current one does not) public_key = owner.get("ecdh_public_key") if isinstance(public_key, str) and public_key.strip(): out["public_key"] = public_key return out + + # ------------------------------------------------------------------------- + # Service 2: Generate-public-key + # ------------------------------------------------------------------------- + def generate_public_key(self, keyring: Dict[str, Any]) -> str: + """ + POST Generate-public-key + body: {"data": {"keyring": {"ecdh": "..."} }} + + Response: + {"ecdh_public_key": ""} + + Returns the public key string. + """ + keyring = self._require_dict("keyring", keyring) + + res = self._post_data( + "Generate-public-key", + {"keyring": keyring}, + ) + + pub = res.get("ecdh_public_key") + if not isinstance(pub, str) or not pub.strip(): + raise ZenroomServiceError(f"Invalid public key response: {res!r}") + return pub + + # ------------------------------------------------------------------------- + # Service 3: Encrypt-a-message-with-the-password (symmetric) + # ------------------------------------------------------------------------- + def symmetric_encrypt(self, *, header: str, message: str, shared_key: str) -> Dict[str, str]: + """ + POST Encrypt-a-message-with-the-password + body: {"data": {"header": "...", "message": "...", "password": "..."}} + + Response: + {"secret_message": {"checksum": "...", "header": "...", "iv": "...", "text": "..."}} + + Returns the inner secret_message dict. + """ + header = self._require_non_empty_str("header", header) + message = self._require_non_empty_str("message", message) + shared_key = self._require_non_empty_str("shared_key", shared_key) + + res = self._post_data( + "Encrypt-a-message-with-the-password", + {"header": header, "message": message, "password": shared_key}, + ) + + sm = res.get("secret_message") + if not isinstance(sm, dict): + raise ZenroomServiceError(f"Invalid encrypt response (missing secret_message): {res!r}") + + self._require_keys(sm, required=("checksum", "header", "iv", "text"), ctx="secret_message") + for k in ("checksum", "header", "iv", "text"): + if not isinstance(sm.get(k), str) or not sm[k].strip(): + raise ZenroomServiceError(f"Invalid secret_message.{k}: {sm!r}") + + return { + "checksum": sm["checksum"], + "header": sm["header"], + "iv": sm["iv"], + "text": sm["text"], + } + + # ------------------------------------------------------------------------- + # Service 4: Decrypt-the-message-with-the-password (symmetric) + # ------------------------------------------------------------------------- + def symmetric_decrypt(self, *, secret_message: Dict[str, Any], shared_key: str) -> str: + """ + POST Decrypt-the-message-with-the-password + body: {"data": {"secret_message": {...}, "password": "..."}} + + Response: + {"textDecrypted": ""} + + Returns decrypted plaintext. + """ + secret_message = self._require_dict("secret_message", secret_message) + shared_key = self._require_non_empty_str("shared_key", shared_key) + + res = self._post_data( + "Decrypt-the-message-with-the-password", + {"secret_message": secret_message, "password": shared_key}, + ) + + txt = res.get("textDecrypted") + if not isinstance(txt, str): + raise ZenroomServiceError(f"Invalid decrypt response: {res!r}") + return txt + + # ------------------------------------------------------------------------- + # Service 5: Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography + # ------------------------------------------------------------------------- + def asymmetric_encrypt( + self, + *, + receiver_public_key: str, + sender_keyring: Dict[str, Any], + header: str, + message: str, + ) -> Dict[str, str]: + """ + POST Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography + + Note: service expects 'reciever' spelling. + + Returns inner 'secret' dict with checksum/header/iv/text. + """ + receiver_public_key = self._require_non_empty_str("receiver_public_key", receiver_public_key) + sender_keyring = self._require_dict("sender_keyring", sender_keyring) + header = self._require_non_empty_str("header", header) + message = self._require_non_empty_str("message", message) + + res = self._post_data( + "Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography", + { + "reciever": {"public_key": receiver_public_key}, + "sender": {"keyring": sender_keyring}, + "header": header, + "message": message, + }, + ) + + sec = res.get("secret") + if not isinstance(sec, dict): + raise ZenroomServiceError(f"Invalid asymmetric encrypt response (missing secret): {res!r}") + + self._require_keys(sec, required=("checksum", "header", "iv", "text"), ctx="secret") + for k in ("checksum", "header", "iv", "text"): + if not isinstance(sec.get(k), str) or not sec[k].strip(): + raise ZenroomServiceError(f"Invalid secret.{k}: {sec!r}") + + return { + "checksum": sec["checksum"], + "header": sec["header"], + "iv": sec["iv"], + "text": sec["text"], + } + + # ------------------------------------------------------------------------- + # Service 6: Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography + # ------------------------------------------------------------------------- + def asymmetric_decrypt( + self, + *, + sender_public_key: str, + receiver_keyring: Dict[str, Any], + secret: Dict[str, Any], + ) -> Dict[str, str]: + """ + POST Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography + + Note: service expects 'reciever' spelling. + + Returns {"header": "...", "text": "..."}. + """ + sender_public_key = self._require_non_empty_str("sender_public_key", sender_public_key) + receiver_keyring = self._require_dict("receiver_keyring", receiver_keyring) + secret = self._require_dict("secret", secret) + + res = self._post_data( + "Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography", + { + "sender": {"public_key": sender_public_key}, + "reciever": {"keyring": receiver_keyring}, + "secret": secret, + }, + ) + + hdr = res.get("header") + txt = res.get("text") + if not isinstance(hdr, str) or not isinstance(txt, str): + raise ZenroomServiceError(f"Invalid asymmetric decrypt response: {res!r}") + + return {"header": hdr, "text": txt} + + # ------------------------------------------------------------------------- + # Service 7: Sign-objects-using-asymmetric-cryptography + # ------------------------------------------------------------------------- + def sign_objects(self, *, objects: Dict[str, Any], signer_keyring: Dict[str, Any]) -> Dict[str, Any]: + """ + POST Sign-objects-using-asymmetric-cryptography + body: {"data": {"mySecretStuff": {...}, "signer": {"keyring": {...}}}} + + Response echoes fields and adds "<field>.signature": {"r": "...", "s": "..."}. + Returns response as-is (validated to contain at least one signature). + """ + objects = self._require_dict("objects", objects) + signer_keyring = self._require_dict("signer_keyring", signer_keyring) + + res = self._post_data( + "Sign-objects-using-asymmetric-cryptography", + {"mySecretStuff": objects, "signer": {"keyring": signer_keyring}}, + ) + + # Validate at least one "*.signature" and that each has r/s. + sig_keys = [k for k in res.keys() if isinstance(k, str) and k.endswith(".signature")] + if not sig_keys: + raise ZenroomServiceError(f"No signatures found in sign response: {res!r}") + + for k in sig_keys: + sig = res.get(k) + if not isinstance(sig, dict): + raise ZenroomServiceError(f"Invalid signature object for {k}: {res!r}") + if not isinstance(sig.get("r"), str) or not isinstance(sig.get("s"), str): + raise ZenroomServiceError(f"Invalid signature fields for {k}: {sig!r}") + + return res + + # ------------------------------------------------------------------------- + # Service 8: Verify-asymmetric-cryptography-signature + # ------------------------------------------------------------------------- + def verify_signature( + self, + *, + message_field: str, + message_value: str, + signature: Dict[str, Any], + signer_public_key: str, + ) -> bool: + """ + POST Verify-asymmetric-cryptography-signature + + Input example uses dynamic field names like: + "myMessage": "...", + "myMessage.signature": {"r": "...", "s": "..."}, + "signer": {"public_key": "..."} + + On success, response includes: + {"output": ["Zenroom_certifies_that_signature_is_correct!"], ...} + + On failure, RESTroom returns zenroom_errors/exception which _post_data raises. + Returns True on success. + """ + message_field = self._require_non_empty_str("message_field", message_field) + message_value = self._require_non_empty_str("message_value", message_value) + signature = self._require_dict("signature", signature) + signer_public_key = self._require_non_empty_str("signer_public_key", signer_public_key) + + payload: Dict[str, Any] = { + message_field: message_value, + f"{message_field}.signature": signature, + "signer": {"public_key": signer_public_key}, + } + + res = self._post_data("Verify-asymmetric-cryptography-signature", payload) + + out = res.get("output") + if not isinstance(out, list) or not out: + raise ZenroomServiceError(f"Invalid verify response: {res!r}") + + # We accept any non-empty success output, but the canonical string is: + # "Zenroom_certifies_that_signature_is_correct!" + return True + + # ------------------------------------------------------------------------- + # Backward-compatible alias names (used by existing live tests / older code) + # ------------------------------------------------------------------------- + def generate_a_keypair_reading_identity_from_data(self, my_name: str) -> Dict[str, Any]: + return self.generate_keypair(my_name) + + def encrypt_a_message_with_the_password(self, *, header: str, message: str, password: str) -> Dict[str, str]: + return self.symmetric_encrypt(header=header, message=message, shared_key=password) + + def decrypt_the_message_with_the_password(self, *, secret_message: Dict[str, Any], password: str) -> Dict[str, str]: + # Historical alias returned {"textDecrypted": "..."} in some tests; + # keep that shape for compatibility. + txt = self.symmetric_decrypt(secret_message=secret_message, shared_key=password) + return {"textDecrypted": txt} diff --git a/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc b/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc index 55af9bbe605f48575b458d7fe29980a1d407948c..6481604d6fbcdbd515a9c9516a923029d1ad7a87 100644 GIT binary patch literal 13574 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 literal 6790 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 XUzFqgcWy4V^cGuszbE)*?$`eTTKLo( diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/__pycache__/__init__.cpython-313.pyc b/tests/integration/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..871338f757754ea22dadb3173b647bcd26827930 GIT binary patch literal 141 zcmey&%ge<81UZkEWrFC(AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekkl<5{fzwFRQ=N8 z)FS<Y>`eWV)Z&t2{mi_Q)bygnlFa-({rLFIyv&mLc)fzkTO2mI`6;D2sdh!IK+Pb- Qi$RQ!%#4hTMa)1J0Dz$&`~Uy| literal 0 HcmV?d00001 diff --git a/tests/integration/__pycache__/test_zenroom_live.cpython-313.pyc b/tests/integration/__pycache__/test_zenroom_live.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25969f109beef68ec3851de13bb9117d8a6ac6b7 GIT binary patch literal 4184 zcmcIn-ER}w6~FUgd;FCUVv|4{b}($gOC0l+uw=JEAqg}KNsuQ-2%;TL>`O8md&a#p z4mj1Stg3bst*Q`Jh#*yhR;tv#^??We4BTXE_d=yg-9GGFQg7dT?u;jd5O(QGy^@db z$N9SVp7XnB8qsJ7LHls(4|+X_&_CJ0YyM`Z@ji#peWW0TyNrnY&k@cU^Mr?g{<83q zNJJS;i3v32>ji$+G7<NGJAK<ZMM#AaB7rGBvuC@!+t0MWKBu6m5Lgd*)_ZxEM`j+2 zm%|@LNCf)6spuJA@gG2+h@@qT%N%@N6KH8kpx)+!67V#2q6AW;14s#iocy|6ui(yk zlB8{hONPE?ljBK_idQvzmIiW~nllN8iP#Nn5YsFwn9S=rJXFxJVN*ZBc9|Ht?fOL+ zFy)QAK<=ZPK;4#UZqggG|BP?@$n7hf!h74u1YTeFT+Toc>}=keB76W#M^I86r+YHl zarMgRwGs8k$arRAVvM2z<{;%xQlWw^D%zxslRnCu77buyUMHqOg&Ay9pJfxhM12MG zHYR^U4Q7t^rf1C}PM0lA(xo{)ZDY%}(z;>e8KT*`X*e_Lf_q?UL7&Gxr3#HYJrx_; zbOGl{98^W&XPpId7rh8am;0Ccmj{*xo*n4^<MdkiWG#HQ8a}%gK35B$uZGXBg$Fkf zf06TVB0l1WuFp?cXe|sV*p_$h@y91++muP!kePkCd3!SW%&t$iisl@a9nW96JnxDc zCqVOkG=*61K;Cdfm=YEKz*eLYpCTMU#IJ}z1B#DGGyY_Nc1*%yG>u&W!_9E9zGj~0 znz;sDgjmKpVbbGd?Ef!J4DNgA78LPqLeSuEZUy1@u;Nqvo)r%X4B;kViQBSVmd^@G z;GB>}1rOoS3`$B6)Xi8{FAORpYBqCaAwFx{rT%ofVCJ;KtZCW(r%#_aLHBx$gI7n@ ztC^9Dqt_S2v{uq-o3}DKIyN$qom3UdpE{8gs5GrvSS^zR4FR88BKVemhX!rEh|RLC zTFC%mkxN<_X=Q}L9u*)32+L>c&pGgr6seFaScJhAd4~}hh{XUxhDKu0p&<*~+0sx! zvn-l;L-h2#<k`<O7(ni#uiE1e`d9iN46F=1OPqPuH@4P3UTdGMwok6LUt5%3#5*3` zSh?}w<CTw}9eMxhkJjRYwfLoK{L)%{bTRZI*6~~ZQRrdlQOm=YXGi;=4zG0%uEj1a z2EU3Y>+!C7$D#VZcQym!zR;rZ>yVRauf*>b%=J0(n?)ILYG=!^2;b2ZZtJc&klGCZ z`7Gzfg^b?7BkbVW1t_xsq)h!y$#dZqt-DJI-y*RUnVvp`JR9Dg9DjG`b2Pu?n-P<~ z1v|v9-0Ba>^3@4tQf7gfzJv`7NQzThY7SRQnohb?1Z#QSm`Uk*cz1NWlDb9AVk)oM z8WYtj1yjq*w~1CN;k>L_^3M`%Ezu91Vr5(_V(2D%dU_I{ej1`J44Sflg(pv)KGWMr zL#6U`LC>jRjfP7^p9kY?FrM_21niZjW|}Uk8k=V>sZEbm&m)z2q%x~if?&qP9yREm zX3rWmEL$^jm<sTuKwdcvlkDB$sc7NCEh-RfSqy)@%s9&6`EAUde^c7LrGj_CsP#)A zchN=!wYD$MEzQ+hE>&AD)gwP_2z)D~z=ni^(dFn;^j_id(7JT2E=8PC@v*WlC1F&P zx~fvwYVL7&^<er*`?}P-eKJ)&c;d<7b*YcZwJx=;3aeMDoym3Sy_XYX)z0_6l#aii zYFkgmfRkzpXRyQpe|NTB(+!eI0XRP=7`)<ZM?X1_kO6?YTjE_m^<4>a{qX;m9xngO zpdTAVD2$vW+37rtGYnRDv@8$a`_J))2tG5g-O;jQrftU*451K&vtn&KOw35h;KC=n zrQC>-BbAa38CQfT)Mjw%wr<a+>{*;DL9E_3N&b}-9mXy{V|cT~!l_}b5&42S-7Qzl zGLa2PL|M0>G%;ms)+BbfJY$;q=?a!p@*t!j?pf#<l;<(2$a=8^44Pp>(UOBYlg5gb zYp|d=qpMA?;JNb80ybbm15HP$<hcwFkd&LnxjCy`B&;-~ygm=*9=>Bk+O@cP)utF> zg&2*p#pdpCemog;0pKup4+!$V$*!^kQepq9XN?>O1*Dsi6eB%M-@AGDq#VvSiqfzP z2qQl&YX#>?P5MZh$p=dXjg^*nY#MC(fb*1_qrs+PoVQp=vIn*KqPn=@Iqcc@yi|!< zHu6qc`|Y^#-3t1^uJwB$4sOIpxy9f=Vx12zuUxLhhN`ildV6<0)>V%m-w2@iFt>rk zcxW-$h@icFi-YyZp5?KnvDHKCk)zPBMGjRXhaM+?*Zo9TkDOtv6H61TqYtnAHIm#6 zfXGG&#rM0)N2;-r`VWpU%{~;r2$}_^890K`n$%g9I#>5U>U!Aq_~&c;&pzG%r8MM# z3fT+3!j)1<RaqpeDh;V>(ae_%tRGQTcKpqiR#m;F6U#2>1~yDs4(C{nQpEPGnI}g; z6FJGqFc2D7Re*OJo)FE3qn$1T@~Wz1&$Y}-9n#?uRt#H*fc%O9uD@-Q5Bfq40eTy( z_wRkJzV81dI(@^OE6oIxd!3?-N@YX08L-1kQi1Q45`3r?^l9RJ(ZI%T>CR^SC5^Bz zv|!H6vk--^Inkkh-C%E%6QZ=s$zrp|`&NM!_IaSX^_yGFiPNT8a6VU{R-4mHS)%|D zK_&L+3_+pjZi^l5%dJxBL-$w*$aPp?P-HCw*$_C6dyaz7QR{OQ`a9~}^rL9oz0NPj z-TSXaj#VSaYLRp`lKykD8u{REu#SY!rO%|#TR&^P$E}>cSNYArI!gQt4Q%)Y&VSGT z2EiYkA|I-8vA=M!zlm+Xn5c`x4WGdC-$-Y<VQzJ{b}$XEQUkG3BM1wd&Vu9ce*sOg Bzw7`2 literal 0 HcmV?d00001 diff --git a/tests/integration/__pycache__/test_zenroom_service_client_integration.cpython-313.pyc b/tests/integration/__pycache__/test_zenroom_service_client_integration.cpython-313.pyc index b892a8d01626c1a80e7e36b6818bbde773c91192..552bde6abe99a6929dfa70f49a413961a3cdbd86 100644 GIT binary patch delta 1067 zcmZuvOHUI~6rM+CIxWl)Xd7OVOahe_c?1v;K@t>WNQ6k6F-8`Xp}m#~otb*?6fGN! z3sY8_%myC`m>2_z@v(8|+MQ`Frp?s_3x5ElaqpcXQGzF#bMN`i<NMC!It`y1rK%|U zDPq?uul2W{50b$6c@nbZtG;k#=3%GLO`o(lr!XVT5hha#?Yv~pbdAtQfXOOUOg!1( zMhd$qIn!*&fo#B|u(5!1$b{E<)1%O_(CPBHSMkJ!xNn>_Ia1Mlx<cVd-apQmJRxj7 zoxhd*t1(4I>*k@mX_Usol7@hqg=rjGpEYfef|{*crh)(*92!19GK#$`AP03bh3hg< zU)5}&5@2m;253;eY%TT5IV%fgQ!{|9BiXhr87*7T?v+!Pm0ZdJxlf)>>j}`m);uk* z0+^F^BSQ)_)0PoPKw#sbk-G`twOqUw-p~-bXTfBY#r!hRk^o|_0T9ws0Q=Fh20_x5 z5M{7*)UW=@!hpMz=zM1ff`Jq;0oetpnd}mwcRL)Pr)L>AqMTs>8xjnd37E^+|3XP3 z!G-FG2mtM?ce7eL>ct)e(swYQNbA6~F|(4vESZaanY5;pzjmKs$iPg-MqZ*pCg#j} z7Ft3!KsKRCs<u@-kYTL=Jnnp?JJ<XFo0LdD`b^vd>Sts44Sct@yH>b(y*MyeY@E;Y z2RtPO%VJki>?(vOOX8(GQxWPPiyPt-eY<H-=sJ#!?bx0W*(b$Pq$ovpns#P)J4(`6 zo+TA!p`*AibZjPG4etrvZcD>P!zR1sd+K{$M}`oV&@7$zRm4D93>U?4CD2<5c2t`C z4!o3f_JHz8{ycNwuW51f?_-6T+XXRP5EpBL%0f#~XxVJ~CUjTHF4QhHAN6BFRo!=@ zs@Sip&KGV<avwY9Q@{r1DvUa9e7n|5Ab8Go5!VUFuAW>_A90VUeNHTMEnhi8u+{pk zwa__IY8@?HSSWF^Lng>Ysw~CF>8g7@n&b|NHb9pR4mz9s6}sK|!H>0_<if(FVI{L^ cFa@WaKJP-PnU3s4$~_aso{3)+32MWB0~t*#;Q#;t delta 769 zcmb7C&ubGw6rN_Y>m=D9O*F97G;P|XxRH|7&_+%1QV>LIX+!iNvZQegi<>meZi4nw z@gR+t2zv>5*@J?jAVRMm^e@Po$fkQS_!m^7cV|<A6hv@fzIpF^Z@zDM%*UW1@I#LC z1F|aSE9EWqfrlQym!uTg!-vMK=gCIEEuUNkPu@!t>JB^Rmp#YPEGU32Va)zH$2Ycu z#uAmdOBXyjQY@|lxBu@B^nW{yZj2n_Oc|7c#2BBb)8~b2RUMUaQCD`V1r1>*P{T^2 zs3Z9SdgS<P^=(xtxv}IkUU*Z+H?K4$QI1zb>V1bPsS2v<4uz4Xxl=J}^k!mCN185| zQ56x{kxs3sU>T#LDieo>QbNvE<B*p2{@KG5J?$ld$Ka<Qgcjc6_q%U)JC|=*sdX!O zbDtUTXKX%U@rlktw##4Krv{L1aZT=d#mx7h)Q7AMr!6?$E}4r~G<`7NgDZq=2~D9* zwbw22deRotmYD8}7kV&r!~j-oXS#uCpPjVXn8n7-c~iCKG6(q{dx?;(jpjys&19{} zq8-UtkxVzT{FPlfq5(T&d}nU$O_J9-Od08@+Q1Pv3Cpskli`-+qON0QyG~{!%Xo&= zaMXpfF2FIkUq(w2cnStIXVc;rTI@rA%lFLJ3B|iG(TA}iO)<<5$kN%Nk3>T^I@aP` z9ZA6MGjQDad}?hXY%nl8b<)m(tCgL5broH~OU7-u84gWkU!Sv+IV+j_1xPYd`wj44 Bz*_(S diff --git a/tests/integration/test_zenroom_service_client_integration.py b/tests/integration/test_zenroom_service_client_integration.py index 87c79b3..90947ca 100644 --- a/tests/integration/test_zenroom_service_client_integration.py +++ b/tests/integration/test_zenroom_service_client_integration.py @@ -11,20 +11,25 @@ from crypto.zenroom_service_client import ZenroomServiceClient class TestZenroomServiceClientIntegration(unittest.TestCase): - @unittest.skipUnless( - os.getenv("ZENROOM_BASE_URL"), - "No ZENROOM_BASE_URL set", - ) - def test_generate_keypair_real_service(self): + @unittest.skipUnless(os.getenv("ZENROOM_BASE_URL"), "No ZENROOM_BASE_URL set") + def test_end_to_end_smoke(self): client = ZenroomServiceClient(base_url=os.environ["ZENROOM_BASE_URL"]) - res = client.generate_keypair("IntegrationUser") + # 1) keypair -> public key + kp = client.generate_keypair("IntegrationUser123456") + self.assertIn("keyring", kp) + self.assertIn("private_key", kp) - self.assertIn("private_key", res) - self.assertIsInstance(res["private_key"], str) - self.assertTrue(res["private_key"].strip()) + pub = client.generate_public_key(kp["keyring"]) + self.assertIsInstance(pub, str) + self.assertTrue(pub.strip()) - # public_key may or may not be returned by this contract variant - if "public_key" in res: - self.assertIsInstance(res["public_key"], str) - self.assertTrue(res["public_key"].strip()) + # 2) symmetric roundtrip + plaintext = "Dear Bob, your name is too short, goodbye - Alice." + sm = client.symmetric_encrypt( + header="A very important secret", + message=plaintext, + shared_key="myVerySecretPassword", + ) + pt = client.symmetric_decrypt(secret_message=sm, shared_key="myVerySecretPassword") + self.assertEqual(pt, plaintext) diff --git a/tests/test_zenroom_service_client.py b/tests/test_zenroom_service_client.py index 24fccdd..ee36fde 100644 --- a/tests/test_zenroom_service_client.py +++ b/tests/test_zenroom_service_client.py @@ -7,7 +7,7 @@ from pathlib import Path code_path = Path(__file__).parents[1] / "ca_core" sys.path.insert(0, str(code_path)) -from crypto.zenroom_service_client import ZenroomServiceClient +from crypto.zenroom_service_client import ZenroomServiceClient, ZenroomServiceError class _FakeHTTPResponse: @@ -27,41 +27,198 @@ class _FakeHTTPResponse: class TestZenroomServiceClient(unittest.TestCase): @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") - def test_generate_keypair_unpacks_private_key(self, m_urlopen): + def test_generate_keypair_returns_keyring_and_private_key(self, m_urlopen): payload = { - "IntegrationUser": { - "keyring": {"ecdh": "PRIVKEY"}, - } + "User123456": {"keyring": {"ecdh": "PRIVKEY"}} } - m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8")) client = ZenroomServiceClient(base_url="http://localhost:3300") - res = client.generate_keypair("IntegrationUser") + res = client.generate_keypair("User123456") + self.assertEqual(res["my_name"], "User123456") self.assertEqual(res["private_key"], "PRIVKEY") + self.assertEqual(res["keyring"], {"ecdh": "PRIVKEY"}) self.assertNotIn("public_key", res) req = m_urlopen.call_args[0][0] self.assertEqual(req.method, "POST") self.assertTrue(req.full_url.endswith("/api/Generate-a-keypair,-reading-identity-from-data")) - sent = json.loads(req.data.decode("utf-8")) - self.assertEqual(sent, {"data": {"myName": "IntegrationUser"}}) + self.assertEqual(sent, {"data": {"myName": "User123456"}}) @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") - def test_generate_keypair_includes_public_key_if_present(self, m_urlopen): - payload = { - "IntegrationUser": { - "ecdh_public_key": "PUBKEY", - "keyring": {"ecdh": "PRIVKEY"}, - } - } - + def test_generate_public_key_returns_string(self, m_urlopen): + payload = {"ecdh_public_key": "PUBKEY"} m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8")) client = ZenroomServiceClient(base_url="http://localhost:3300") - res = client.generate_keypair("IntegrationUser") + pub = client.generate_public_key({"ecdh": "PRIVKEY"}) + self.assertEqual(pub, "PUBKEY") - self.assertEqual(res["private_key"], "PRIVKEY") - self.assertEqual(res["public_key"], "PUBKEY") + req = m_urlopen.call_args[0][0] + self.assertTrue(req.full_url.endswith("/api/Generate-public-key")) + sent = json.loads(req.data.decode("utf-8")) + self.assertEqual(sent, {"data": {"keyring": {"ecdh": "PRIVKEY"}}}) + + @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") + def test_symmetric_encrypt_returns_secret_message_dict(self, m_urlopen): + payload = { + "secret_message": { + "checksum": "C", + "header": "H", + "iv": "IV", + "text": "T", + } + } + m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8")) + + client = ZenroomServiceClient(base_url="http://localhost:3300") + sm = client.symmetric_encrypt( + header="A very important secret", + message="hello", + shared_key="myVerySecretPassword", + ) + self.assertEqual(sm["checksum"], "C") + self.assertEqual(sm["header"], "H") + self.assertEqual(sm["iv"], "IV") + self.assertEqual(sm["text"], "T") + + req = m_urlopen.call_args[0][0] + self.assertTrue(req.full_url.endswith("/api/Encrypt-a-message-with-the-password")) + sent = json.loads(req.data.decode("utf-8")) + self.assertEqual( + sent, + {"data": {"header": "A very important secret", "message": "hello", "password": "myVerySecretPassword"}}, + ) + + @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") + def test_symmetric_decrypt_returns_plaintext(self, m_urlopen): + payload = {"textDecrypted": "PLAINTEXT"} + m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8")) + + client = ZenroomServiceClient(base_url="http://localhost:3300") + txt = client.symmetric_decrypt(secret_message={"iv": "x"}, shared_key="k") + self.assertEqual(txt, "PLAINTEXT") + + req = m_urlopen.call_args[0][0] + self.assertTrue(req.full_url.endswith("/api/Decrypt-the-message-with-the-password")) + sent = json.loads(req.data.decode("utf-8")) + self.assertEqual(sent, {"data": {"secret_message": {"iv": "x"}, "password": "k"}}) + + @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") + def test_asymmetric_encrypt_returns_secret(self, m_urlopen): + payload = {"secret": {"checksum": "C", "header": "H", "iv": "IV", "text": "T"}} + m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8")) + + client = ZenroomServiceClient(base_url="http://localhost:3300") + sec = client.asymmetric_encrypt( + receiver_public_key="PUB", + sender_keyring={"ecdh": "PRIV"}, + header="hdr", + message="msg", + ) + self.assertEqual(sec, {"checksum": "C", "header": "H", "iv": "IV", "text": "T"}) + + req = m_urlopen.call_args[0][0] + self.assertTrue(req.full_url.endswith("/api/Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography")) + sent = json.loads(req.data.decode("utf-8")) + self.assertEqual( + sent, + { + "data": { + "reciever": {"public_key": "PUB"}, + "sender": {"keyring": {"ecdh": "PRIV"}}, + "header": "hdr", + "message": "msg", + } + }, + ) + + @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") + def test_asymmetric_decrypt_returns_header_and_text(self, m_urlopen): + payload = {"header": "HDR", "text": "TXT"} + m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8")) + + client = ZenroomServiceClient(base_url="http://localhost:3300") + out = client.asymmetric_decrypt( + sender_public_key="PUB", + receiver_keyring={"ecdh": "PRIV"}, + secret={"iv": "IV"}, + ) + self.assertEqual(out, {"header": "HDR", "text": "TXT"}) + + req = m_urlopen.call_args[0][0] + self.assertTrue(req.full_url.endswith("/api/Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography")) + sent = json.loads(req.data.decode("utf-8")) + self.assertEqual( + sent, + { + "data": { + "sender": {"public_key": "PUB"}, + "reciever": {"keyring": {"ecdh": "PRIV"}}, + "secret": {"iv": "IV"}, + } + }, + ) + + @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") + def test_sign_objects_returns_response_and_validates_signatures(self, m_urlopen): + payload = { + "myMessage": "hello", + "myMessage.signature": {"r": "R", "s": "S"}, + } + m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8")) + + client = ZenroomServiceClient(base_url="http://localhost:3300") + res = client.sign_objects(objects={"myMessage": "hello"}, signer_keyring={"ecdh": "PRIV"}) + + self.assertEqual(res["myMessage"], "hello") + self.assertEqual(res["myMessage.signature"]["r"], "R") + + req = m_urlopen.call_args[0][0] + self.assertTrue(req.full_url.endswith("/api/Sign-objects-using-asymmetric-cryptography")) + sent = json.loads(req.data.decode("utf-8")) + self.assertEqual( + sent, + {"data": {"mySecretStuff": {"myMessage": "hello"}, "signer": {"keyring": {"ecdh": "PRIV"}}}}, + ) + + @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") + def test_verify_signature_returns_true(self, m_urlopen): + payload = { + "myMessage": "hello", + "output": ["Zenroom_certifies_that_signature_is_correct!"], + } + m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8")) + + client = ZenroomServiceClient(base_url="http://localhost:3300") + ok = client.verify_signature( + message_field="myMessage", + message_value="hello", + signature={"r": "R", "s": "S"}, + signer_public_key="PUB", + ) + self.assertTrue(ok) + + req = m_urlopen.call_args[0][0] + self.assertTrue(req.full_url.endswith("/api/Verify-asymmetric-cryptography-signature")) + sent = json.loads(req.data.decode("utf-8")) + self.assertEqual( + sent, + {"data": {"myMessage": "hello", "myMessage.signature": {"r": "R", "s": "S"}, "signer": {"public_key": "PUB"}}}, + ) + + @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") + def test_zenroom_error_is_raised(self, m_urlopen): + payload = {"exception": "boom", "zenroom_errors": {"logs": "fail"}} + m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8")) + + client = ZenroomServiceClient(base_url="http://localhost:3300") + with self.assertRaises(ZenroomServiceError): + client.verify_signature( + message_field="myMessage", + message_value="hello", + signature={"r": "R", "s": "S"}, + signer_public_key="PUB", + )