diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md deleted file mode 100644 index 995e8d4..0000000 --- a/PROJECT_CONTEXT.md +++ /dev/null @@ -1,328 +0,0 @@ -CA/PKI Backend Project Context (Updated) -Stack - -Python 3 + psycopg (dict_row cursors) - -PostgreSQL database: ca - -Unit tests: unittest - -Run via: python3 -m unittest discover - -Current test count: 17 - -Database Schema (current assumptions) -entity - -id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY - -creation_ts TIMESTAMPTZ DEFAULT now() - -creator INT FK → entity(id) (nullable) - -name VARCHAR(100) NOT NULL - -type VARCHAR(...) NOT NULL - -Allowed types: person, group, device - -public_key VARCHAR(300) NOT NULL - -symmetrical_key VARCHAR(100) NULL - -status VARCHAR(...) NOT NULL DEFAULT 'active' - -Values: 'active', 'revoked' - -expiration DATE NULL - -ca_reference VARCHAR(100) NULL - -Constraint -CHECK ( - (type = 'group' AND ca_reference IS NOT NULL) - OR - (type <> 'group' AND ca_reference IS NULL) -) - -Rule: - -Groups MUST have a ca_reference - -All other entity types MUST have ca_reference IS NULL - -Indexes: - -Index on entity(name) - -Other indexes as needed - -group_member - -group_id INT FK → entity(id) ON DELETE CASCADE - -member_id INT FK → entity(id) ON DELETE CASCADE - -role VARCHAR(10) - -PRIMARY KEY (group_id, member_id) - -Index (member_id, group_id) - -Groups can contain: - -persons - -devices - -other groups - -property - -id INT FK → entity(id) ON DELETE CASCADE - -property_name VARCHAR(100) NOT NULL - -validation_policy CHAR(19) NOT NULL DEFAULT 'default' - -source VARCHAR(150) NULL - -PRIMARY KEY (id, property_name) - -Notes: - -validation_policy is CHAR(19) and padded by PostgreSQL. - -Used for flags/roles such as "creator". - -metadata - -Singleton row table (enforced at application level). - -Columns: - -name - -comment - -private_key - -public_key - -defense_p BOOLEAN NOT NULL DEFAULT false - -defense_p: - -Global system flag. - -Logged on change. - -log - -id SERIAL PRIMARY KEY - -ts TIMESTAMPTZ DEFAULT now() - -entry TEXT NOT NULL - -Every API mutation must insert exactly one row here. - -Core Business Rules -Creators are NOT an entity type - -"creator" is a property (property_name='creator') on a person. - -insert_creator(): - -Creates a person - -Adds "creator" property - -Revoked entities are immutable - -All entity mutations must call: - -ensure_entity_active(cursor, entity_id) - -Revoked entities CANNOT: - -Join groups - -Accept members - -Add/delete properties - -Change public_key - -Change symmetrical_key - -Change status again - -Group CA Reference Rule - -group entities must include a non-null ca_reference. - -person and device must not define ca_reference. - -Enforced at: - -Database level (CHECK constraint) - -Python validation level - -Property Metadata - -Each property includes: - -validation_policy - -source - -Defaults: - -validation_policy = 'default' - -source = NULL - -Property mutations: - -Require entity to be active - -Must log changes - -Metadata Defense Flag - -defense_p: - -Boolean system-wide flag - -Default: false - -Must be logged when changed - -Logging Rules - -All changes to: - -entity - -group_member - -property - -metadata - -Must call: - -log_change(cursor, "...") - -Logging: - -Happens inside same transaction - -No extra commits - -Exactly one log row per mutation - -Python Modules -ca_core/entity.py - -Provides: - -ensure_entity_active(cursor, entity_id) - -insert_creator(cursor, name, public_key) - -enroll_person(cursor, name, public_key, creator_id) - -create_group(cursor, name, public_key, creator_id, ca_reference) - -get_entity(cursor, entity_id) - -set_entity_status(cursor, entity_id, status, changed_by) - -set_entity_keys(cursor, entity_id, public_key, changed_by) - -set_symmetrical_key(cursor, entity_id, key, changed_by) - -get_symmetrical_key(cursor, entity_id) - -ca_core/group_member.py - -Uses member_id - -Prevents adding revoked groups/members - -Logs membership add/remove - -ca_core/property.py - -Table: - -property(id, property_name, validation_policy, source) - -Rules: - -Reject mutations if entity revoked - -Logs set/delete - -Default policy 'default' - -validation_policy is CHAR(19) - -ca_core/metadata.py - -Updates metadata fields - -Manages defense_p - -Logs changes - -ca_core/db_logging.py -log_change(cursor, message: str) - -Inserts into log(entry). - -Tests - -tests/test_entity.py - -tests/test_group.py - -tests/test_property.py - -tests/test_metadata.py - -Tests verify: - -Creation and enrollment - -Group membership - -Revocation immutability - -CA reference enforcement - -Property metadata fields - -defense_p behavior - -Log entry creation (case-insensitive substring checks) - -Run via: - -python3 -m unittest discover -Known Gotchas - -Do NOT name module logging.py (conflicts with stdlib) - -Schema and code must stay aligned: - -property.id (NOT entity_id) - -group_member.member_id - -entity.ca_reference constraint - -CHAR(19) pads values with spaces diff --git a/PROJECT_CONTEXT.rtf b/PROJECT_CONTEXT.rtf new file mode 100644 index 0000000..e57b979 --- /dev/null +++ b/PROJECT_CONTEXT.rtf @@ -0,0 +1,361 @@ +{\rtf1\ansi\deff3\adeflang1025 +{\fonttbl{\f0\froman\fprq2\fcharset0 Times New Roman;}{\f1\froman\fprq2\fcharset2 Symbol;}{\f2\fswiss\fprq2\fcharset0 Arial;}{\f3\froman\fprq2\fcharset0 Liberation Serif{\*\falt Times New Roman};}{\f4\fswiss\fprq2\fcharset0 Liberation Sans{\*\falt Arial};}{\f5\froman\fprq2\fcharset0 Helvetica{\*\falt Arial};}{\f6\froman\fprq2\fcharset0 Courier{\*\falt Courier New};}{\f7\fnil\fprq2\fcharset0 Noto Sans CJK SC;}{\f8\fnil\fprq2\fcharset0 Noto Sans Devanagari;}{\f9\fswiss\fprq0\fcharset128 Noto Sans Devanagari;}} +{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;} +{\stylesheet{\s0\snext0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052 Normal;} +{\s15\sbasedon0\snext16\rtlch\af8\afs28 \ltrch\hich\af4\loch\sb240\sa120\keepn\f4\fs28\dbch\af7 Heading;} +{\s16\sbasedon0\snext16\loch\sl276\slmult1\sb0\sa140 Body Text;} +{\s17\sbasedon16\snext17\rtlch\af9 \ltrch List;} +{\s18\sbasedon0\snext18\rtlch\af9\afs24\ai \ltrch\loch\sb120\sa120\noline\fs24\i caption;} +{\s19\sbasedon0\snext19\rtlch\af9 \ltrch\loch\noline Index;} +}{\*\generator LibreOffice/25.2.7.2$Linux_X86_64 LibreOffice_project/520$Build-2}{\info{\creatim\yr0\mo0\dy0\hr0\min0}{\revtim\yr0\mo0\dy0\hr0\min0}{\printim\yr0\mo0\dy0\hr0\min0}}{\*\userprops}\deftab720 +\hyphauto1\viewscale100\formshade\paperh15840\paperw12240\margl1440\margr1440\margt1440\margb1440\sectd\sbknone\sftnnar\saftnnrlc\sectunlocked1\pgwsxn12240\pghsxn15840\marglsxn1440\margrsxn1440\margtsxn1440\margbsxn1440\ftnbj\ftnstart1\ftnrstcont\ftnnar\fet\aftnrstcont\aftnstart1\aftnnrlc +{\*\ftnsep\chftnsep}\pgndec\pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs36\b\f5\loch +CA/PKI Backend Project Context} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs32\b\f5\loch +Stack} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab Python 3.13} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab FastAPI (web API)} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab psycopg (PostgreSQL driver)} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab PostgreSQL database: }{\hich\af6\loch\f6\loch +ca} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab Unit tests: }{\hich\af6\loch\f6\loch +unittest} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab HTTP testing: }{\hich\af6\loch\f6\loch +fastapi.testclient} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab Zenroom cryptographic runtime (Docker container)} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Run tests:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af6\loch\f6\loch +python3 -m unittest discover} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Integration tests require:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af6\loch\f6\loch +export DATABASE_URL="postgresql:///ca"} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\qc\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5 +\u8212\'97\u8212\'97\u8212\'97\u8212\'97\u8212\'97} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs36\b\f5\loch +Project Structure} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af6\loch\f6\loch +pki/\line \u9500\'3f\u9472\'3f\u9472\'3f ca_core/\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f entity.py\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f group_member.py\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f property.py\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f metadata.py\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f db_logging.py\line \u9474\'3f \u9492\'3f\u9472\'3f\u9472\'3f crypto/\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f zenroom_client.py\line \u9474\'3f \u9492\'3f\u9472\'3f\u9472\'3f zenroom_service_client.py\line \u9474\'3f\line \u9500\'3f\u9472\'3f\u9472\'3f ca_api/\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f app.py\line \u9474\'3f \u9492\'3f\u9472\'3f\u9472\'3f db.py\line \u9474\'3f\line \u9500\'3f\u9472\'3f\u9472\'3f tests/\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f test_entity.py\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f test_group.py\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f test_property.py\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f test_metadata.py\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f test_api_smoke.py\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f test_api_unit.py\line \u9474\'3f \u9500\'3f\u9472\'3f\u9472\'3f test_api_integration.py\line \u9474\'3f \u9492\'3f\u9472\'3f\u9472\'3f integration/\line \u9474\'3f \u9492\'3f\u9472\'3f\u9472\'3f zenroom tests\line \u9474\'3f\line \u9500\'3f\u9472\'3f\u9472\'3f create_tables.sql\line \u9492\'3f\u9472\'3f\u9472\'3f PROJECT_CONTEXT.md} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\qc\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5 +\u8212\'97\u8212\'97\u8212\'97\u8212\'97\u8212\'97} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs36\b\f5\loch +Architecture} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af6\loch\f6\loch +HTTP API (FastAPI)\line \u9474\'3f\line \u9660\'3f\line ca_api (thin HTTP adapter)\line \u9474\'3f\line \u9660\'3f\line ca_core (business logic)\line \u9474\'3f\line \u9660\'3f\line PostgreSQL} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +The system is layered to keep business logic independent from the HTTP interface.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\qc\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5 +\u8212\'97\u8212\'97\u8212\'97\u8212\'97\u8212\'97} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs36\b\f5\loch +Database Overview} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Database: }{\hich\af5\loch\b\f5\loch +ca} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Core tables:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab entity} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab group_member} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab property} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab metadata} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab log} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs32\b\f5\loch +Entity Rules} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Entity types:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab person} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab group} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab device} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Status values:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab active} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab revoked} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Groups must include }{\hich\af6\loch\f6\loch +ca_reference}{\hich\af5\loch\f5\loch +.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Persons and devices must }{\hich\af5\loch\b\f5\loch +not}{\hich\af5\loch\f5\loch + include }{\hich\af6\loch\f6\loch +ca_reference}{\hich\af5\loch\f5\loch +.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Revoked entities are immutable.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\qc\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5 +\u8212\'97\u8212\'97\u8212\'97\u8212\'97\u8212\'97} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs36\b\f5\loch +Logging} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +All mutations must call:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af6\loch\f6\loch +log_change(cursor, message)} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Exactly }{\hich\af5\loch\b\f5\loch +one log entry must be produced per mutation}{\hich\af5\loch\f5\loch +.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Logging occurs inside the same transaction.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\qc\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5 +\u8212\'97\u8212\'97\u8212\'97\u8212\'97\u8212\'97} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs36\b\f5\loch +Core Modules} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs32\b\f5\loch +entity.py} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Provides:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab insert_creator} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab enroll_person} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab create_group} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab get_entity} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab set_entity_status} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs32\b\f5\loch +group_member.py} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Provides:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab add_group_member} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab remove_group_member} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab get_members_of_group} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs32\b\f5\loch +property.py} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Provides:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab set_property} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab delete_property} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab get_properties} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs32\b\f5\loch +metadata.py} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Provides:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab get_name} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab get_comment} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab get_public_key} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab get_defense_p} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab set_name} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab set_defense_p} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\qc\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5 +\u8212\'97\u8212\'97\u8212\'97\u8212\'97\u8212\'97} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs36\b\f5\loch +Cryptographic Layer} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +The system integrates }{\hich\af5\loch\b\f5\loch +Zenroom}{\hich\af5\loch\f5\loch + for cryptographic operations.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Zenroom runs in an isolated environment, typically a }{\hich\af5\loch\b\f5\loch +Docker container}{\hich\af5\loch\f5\loch +.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af6\loch\f6\loch +ca_core\line \u9474\'3f\line \u9660\'3f\line crypto clients\line \u9474\'3f\line \u9660\'3f\line Zenroom runtime} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs32\b\f5\loch +zenroom_client.py} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Local execution interface.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Responsibilities:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab execute Zenroom scripts} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab parse output} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab raise errors} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Used for development and some integration tests.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs32\b\f5\loch +zenroom_service_client.py} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +HTTP client for a Zenroom service container.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Responsibilities:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab send scripts} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab pass JSON input} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab return parsed result} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Typical flow:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af6\loch\f6\loch +Python\line \u8595\'3f\line ZenroomServiceClient\line \u8595\'3f\line HTTP request\line \u8595\'3f\line Zenroom container\line \u8595\'3f\line JSON result} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\qc\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5 +\u8212\'97\u8212\'97\u8212\'97\u8212\'97\u8212\'97} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs36\b\f5\loch +API Layer (ca_api)} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +FastAPI provides the HTTP interface.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Responsibilities:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab routing} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab validation} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab DB connection handling} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab error mapping} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Flow:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af6\loch\f6\loch +HTTP request\line \u8595\'3f\line FastAPI route\line \u8595\'3f\line db_cursor()\line \u8595\'3f\line ca_core function\line \u8595\'3f\line commit / rollback} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\qc\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5 +\u8212\'97\u8212\'97\u8212\'97\u8212\'97\u8212\'97} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs36\b\f5\loch +Testing Strategy} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Three layers of tests exist.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs32\b\f5\loch +Core Unit Tests} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Test domain logic directly.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Examples:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab test_entity.py} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab test_group.py} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab test_property.py} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab test_metadata.py} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs32\b\f5\loch +API Unit Tests} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Test HTTP behaviour without DB.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Mocked components:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab db_cursor} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab ca_core functions} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +File:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab test_api_unit.py} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs32\b\f5\loch +API Integration Tests} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Full stack tests:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af6\loch\f6\loch +FastAPI\line \u8595\'3f\line ca_core\line \u8595\'3f\line PostgreSQL} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +File:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab test_api_integration.py} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Requires:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af6\loch\f6\loch +export DATABASE_URL="postgresql:///ca"} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs32\b\f5\loch +Zenroom Integration Tests} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Located in:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af6\loch\f6\loch +tests/integration/} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Verify:} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab Zenroom script execution} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab service communication} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi-360\li360\lin360\sb0\sa0\ltrpar{\hich\af5\loch\f5 +\u8226\'95}{\hich\af5\loch\f5\loch +\tab error handling} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +Some tests require a running Zenroom container.} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\qc\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5 +\u8212\'97\u8212\'97\u8212\'97\u8212\'97\u8212\'97} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\fs36\b\f5\loch +Runtime Deployment Model} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af6\loch\f6\loch +FastAPI API\line \u9474\'3f\line \u9660\'3f\line ca_core\line \u9474\'3f\line \u9660\'3f\line crypto client\line \u9474\'3f\line \u9660\'3f\line Zenroom container\line \u9474\'3f\line \u9660\'3f\line PostgreSQL} +\par \pard\plain \s0\rtlch\af8\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar1\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af10\langfe2052\ql\fi0\li0\lin0\sb0\sa180\ltrpar{\hich\af5\loch\f5\loch +This design keeps cryptographic execution isolated while keeping the Python service lightweight.} +\par } \ No newline at end of file diff --git a/__pycache__/test.cpython-313.pyc b/__pycache__/test.cpython-313.pyc new file mode 100644 index 0000000..6797c13 Binary files /dev/null and b/__pycache__/test.cpython-313.pyc differ diff --git a/__pycache__/test2.cpython-313.pyc b/__pycache__/test2.cpython-313.pyc new file mode 100644 index 0000000..99850d5 Binary files /dev/null and b/__pycache__/test2.cpython-313.pyc differ diff --git a/__pycache__/test3.cpython-313.pyc b/__pycache__/test3.cpython-313.pyc new file mode 100644 index 0000000..9dbe2f1 Binary files /dev/null and b/__pycache__/test3.cpython-313.pyc differ 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 414e968..9067c04 100644 Binary files a/ca_core/crypto/__pycache__/zenroom_service_client.cpython-313.pyc and b/ca_core/crypto/__pycache__/zenroom_service_client.cpython-313.pyc differ diff --git a/ca_core/crypto/zenroom_client.py b/ca_core/crypto/zenroom_client.py deleted file mode 100644 index 9f3eb7d..0000000 --- a/ca_core/crypto/zenroom_client.py +++ /dev/null @@ -1,157 +0,0 @@ -import json -import subprocess -import tempfile -from pathlib import Path -from typing import Any, Dict, Optional, Sequence, Union - - -JsonLike = Union[Dict[str, Any], Sequence[Any], str, int, float, bool, None] - - -class ZenroomError(RuntimeError): - """Raised when Zenroom execution fails.""" - - -class ZenroomDockerClient: - """Run Zenroom via Docker. - - This wrapper is intentionally small and testable. It focuses on: - - Writing inputs (script/data/keys/conf) to a temp workdir - - Running a docker container that executes Zenroom - - Returning parsed JSON output when possible - - Assumed container interface (common Zenroom CLI pattern): - zenroom -z -a -k -c - - If your docker image/entrypoint differs, pass `zenroom_args` accordingly. - - Note: This module is named *zenroom_client.py* (not zenroom.py) to avoid - potential import shadowing with future packages/modules. - """ - - def __init__( - self, - image: str = "zenroom/zenroom:latest", - docker_bin: str = "docker", - work_mount_path: str = "/work", - zenroom_args: Optional[Sequence[str]] = None, - timeout_s: int = 30, - ) -> None: - self.image = image - self.docker_bin = docker_bin - self.work_mount_path = work_mount_path - self.zenroom_args = list(zenroom_args) if zenroom_args is not None else ["zenroom", "-z"] - self.timeout_s = timeout_s - - def run( - self, - script: str, - *, - data: Optional[JsonLike] = None, - keys: Optional[JsonLike] = None, - conf: Optional[JsonLike] = None, - extra_docker_args: Optional[Sequence[str]] = None, - extra_zenroom_args: Optional[Sequence[str]] = None, - ) -> Union[dict, str]: - """Execute a Zenroom script. - - Args: - script: The Zenroom script (text). - data/keys/conf: Optional JSON-like payloads written to files. - extra_docker_args: Optional extra args inserted after `docker run`. - extra_zenroom_args: Optional extra args appended before the script path. - - Returns: - Parsed JSON dict if stdout is valid JSON, otherwise raw stdout string. - - Raises: - ZenroomError on non-zero exit. - """ - if not isinstance(script, str) or not script.strip(): - raise ValueError("script must be a non-empty string") - - with tempfile.TemporaryDirectory(prefix="zenroom_") as tmpdir: - workdir = Path(tmpdir) - - # Defensive: TemporaryDirectory normally creates the dir, but tests may mock it. - workdir.mkdir(parents=True, exist_ok=True) - - script_path = workdir / "script.zen" - script_path.write_text(script, encoding="utf-8") - - data_path = None - keys_path = None - conf_path = None - - if data is not None: - data_path = workdir / "data.json" - data_path.write_text(json.dumps(data), encoding="utf-8") - if keys is not None: - keys_path = workdir / "keys.json" - keys_path.write_text(json.dumps(keys), encoding="utf-8") - if conf is not None: - conf_path = workdir / "conf.json" - conf_path.write_text(json.dumps(conf), encoding="utf-8") - - cmd = [ - self.docker_bin, - "run", - "--rm", - "-i", - ] - - if extra_docker_args: - cmd.extend(list(extra_docker_args)) - - # Mount temp dir into container - cmd.extend( - [ - "-v", - f"{workdir}:{self.work_mount_path}", - "-w", - self.work_mount_path, - self.image, - ] - ) - - # Build zenroom command - cmd.extend(list(self.zenroom_args)) - - if data_path is not None: - cmd.extend(["-a", str(Path(self.work_mount_path) / data_path.name)]) - if keys_path is not None: - cmd.extend(["-k", str(Path(self.work_mount_path) / keys_path.name)]) - if conf_path is not None: - cmd.extend(["-c", str(Path(self.work_mount_path) / conf_path.name)]) - - if extra_zenroom_args: - cmd.extend(list(extra_zenroom_args)) - - cmd.append(str(Path(self.work_mount_path) / script_path.name)) - - result = subprocess.run( - cmd, - capture_output=True, - text=True, - timeout=self.timeout_s, - check=False, - ) - - if result.returncode != 0: - stderr = (result.stderr or "").strip() - stdout = (result.stdout or "").strip() - msg = stderr or stdout or f"Zenroom failed with exit code {result.returncode}" - raise ZenroomError(msg) - - out = (result.stdout or "").strip() - if not out: - return "" - - try: - parsed = json.loads(out) - if isinstance(parsed, dict): - return parsed - # Zenroom can output arrays too; keep compatibility. - return {"result": parsed} - except json.JSONDecodeError: - return out diff --git a/ca_core/crypto/zenroom_service_client.py b/ca_core/crypto/zenroom_service_client.py index 09300e8..d5c424d 100644 --- a/ca_core/crypto/zenroom_service_client.py +++ b/ca_core/crypto/zenroom_service_client.py @@ -1,99 +1,122 @@ import json -import urllib.request -import urllib.error +import re from typing import Any, Dict, Optional, Tuple +from zenroom import zenroom + class ZenroomServiceError(RuntimeError): pass class ZenroomServiceClient: + """ + Local Zenroom client using the installed `zenroom` Python wrapper. - def __init__( - self, - base_url: str = "http://localhost:3300", - *, - api_prefix: str = "/api", - timeout_s: int = 10, - ) -> None: - self.base_url = base_url.rstrip("/") - self.api_prefix = api_prefix.strip() + This preserves the public API of the old HTTP/Docker-backed client as much + as possible, so callers should not need changes. + """ - if self.api_prefix in {"", "/"}: - self.api_prefix = "" - elif not self.api_prefix.startswith("/"): - self.api_prefix = "/" + self.api_prefix + SCRIPT_GENERATE_KEYPAIR = """Scenario 'ecdh': Create the keypair from a name passed from data/keys - self.timeout_s = timeout_s +# Here we load the identity of the executor +Given my name is in a 'string' named 'myName' - def _make_url(self, path: str) -> str: - path = "/" + path.lstrip("/") - return f"{self.base_url}{self.api_prefix}{path}" +# Here we generate and print the keypair +When I create the ecdh key +Then print my 'keyring' +""" - def _request_json( - self, - method: str, - path: str, - payload: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: + SCRIPT_GENERATE_PUBLIC_KEY = """# Loading scenarios +Scenario 'ecdh': Create the public key - url = self._make_url(path) - data = None - headers = {"Accept": "application/json"} +# Loading the private keys +Given I have the 'keyring' - if payload is not None: - data = json.dumps(payload).encode("utf-8") - headers["Content-Type"] = "application/json" +# Generating the public keys +When I create the ecdh public key - req = urllib.request.Request(url, data=data, headers=headers, method=method.upper()) +# Here we print all the output +Then print the 'ecdh public key' +""" - try: - with urllib.request.urlopen(req, timeout=self.timeout_s) as resp: - raw = resp.read() - text = raw.decode("utf-8") - except urllib.error.HTTPError as e: - body = "" - try: - body = e.read().decode("utf-8") - except Exception: - pass - raise ZenroomServiceError(f"HTTP {e.code} from {url}: {body or e.reason}") from e - except urllib.error.URLError as e: - raise ZenroomServiceError(f"Failed to reach {url}: {e.reason}") from e + SCRIPT_SYMMETRIC_ENCRYPT = """Scenario 'ecdh': Encrypt a message with the password +Given that I have a 'string' named 'password' +Given that I have a 'string' named 'header' +Given that I have a 'string' named 'message' +When I encrypt the secret message 'message' with 'password' +Then print the 'secret message' +""" - text = text.strip() - if not text: - raise ZenroomServiceError(f"Empty response from {url}") + SCRIPT_SYMMETRIC_DECRYPT = """Scenario 'ecdh': Decrypt the message with the password +Given that I have a valid 'secret message' +Given that I have a 'string' named 'password' +When I decrypt the text of 'secret message' with 'password' +When I rename the 'text' to 'textDecrypted' +Then print the 'textDecrypted' as 'string' +""" - try: - parsed = json.loads(text) - except json.JSONDecodeError as e: - raise ZenroomServiceError(f"Non-JSON response from {url}: {text[:200]}") from e + SCRIPT_ASYMMETRIC_ENCRYPT = """Scenario 'ecdh': Alice encrypts a message for Bob - if not isinstance(parsed, dict): - raise ZenroomServiceError(f"Expected JSON object from {url}") +Given that I am known as 'sender' +Given that I have my valid 'keyring' +Given that I have a valid 'public key' from 'reciever' +Given that I have a 'string' named 'message' +Given that I have a 'string' named 'header' - return parsed +When I encrypt the secret message of 'message' for 'reciever' +When I rename the 'secret message' to 'secret' - def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]: - return self._request_json("POST", path, payload) +Then print the 'secret' +""" - 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}) + SCRIPT_ASYMMETRIC_DECRYPT = """Scenario 'ecdh': Bob decrypts the message from Alice +Given that I am known as 'reciever' +Given I have my 'keyring' +Given I have a 'public key' from 'sender' +Given I have a 'secret message' named 'secret' +When I decrypt the text of 'secret' from 'sender' +Then print the 'text' as 'string' +Then print the 'header' from 'secret' as 'string' +""" - # 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") - logs = "" - if isinstance(ze, dict): - logs = str(ze.get("logs", ""))[:800] - raise ZenroomServiceError(f"Zenroom error from {path}: {exc or logs or 'unknown error'}") + # Used as a template so sign_objects() can sign any single string field. + SCRIPT_SIGN_TEMPLATE = """Scenario 'ecdh': create the signature of an object +Given I am 'signer' +Given I have my 'keyring' +Given that I have a 'string' named '{field_name}' inside 'mySecretStuff' - return res +When I create the ecdh signature of '{field_name}' +When I rename the 'ecdh signature' to '{field_name}.signature' + +Then print the '{field_name}' +Then print the '{field_name}.signature' +""" + + # Used as a template so verify_signature() can verify any single string field. + SCRIPT_VERIFY_TEMPLATE = """rule check version 3.0.0 +Scenario 'ecdh': Bob verifies the signature from Alice + +# Here we load the pubkey we'll verify the signature against +Given I have a 'public key' from 'signer' + +# Here we load the objects to be verified +Given I have a 'string' named '{field_name}' + +# Here we load the objects's signatures +Given I have a 'signature' named '{field_name}.signature' + +# Here we perform the verifications +When I verify the '{field_name}' has a ecdh signature in '{field_name}.signature' by 'signer' + +# Here we print out the result: if the verifications succeeded, a string will be printed out +# if the verifications failed, Zenroom will throw an error +Then print the string 'Zenroom certifies that signature is correct!' +Then print the '{field_name}' +""" + + def __init__(self) -> None: + pass @staticmethod def _require_non_empty_str(name: str, value: str) -> str: @@ -116,6 +139,68 @@ class ZenroomServiceClient: if missing: raise ZenroomServiceError(f"Missing {missing} in {ctx}: {d!r}") + @staticmethod + def _require_safe_field_name(field_name: str) -> str: + """ + Restrict dynamic field names used inside generated Zencode to avoid script injection. + """ + if not isinstance(field_name, str): + raise TypeError("field_name must be a string") + if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", field_name): + raise ValueError( + "field_name must match [A-Za-z_][A-Za-z0-9_]* for safe Zencode generation" + ) + return field_name + + def _run_script( + self, + script_name: str, + script_text: str, + *, + data: Optional[Dict[str, Any]] = None, + keys: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """ + Execute a local Zencode script and return parsed JSON result as a dict. + """ + try: + result = zenroom.zencode_exec( + script_text, + data=json.dumps(data or {}), + keys=json.dumps(keys) if keys is not None else None, + ) + except Exception as e: + raise ZenroomServiceError(f"Failed to execute Zenroom script {script_name}: {e}") from e + + logs = getattr(result, "logs", None) + output = getattr(result, "output", None) + parsed = getattr(result, "result", None) + + if isinstance(parsed, dict): + return parsed + + # Fallback: sometimes output may still be JSON text even if .result is None. + if isinstance(output, str): + try: + parsed_output = json.loads(output) + except json.JSONDecodeError: + parsed_output = None + if isinstance(parsed_output, dict): + return parsed_output + + log_text = logs + if isinstance(log_text, list): + log_text = "\n".join(str(x) for x in log_text) + elif log_text is None: + log_text = "" + + out_preview = output if isinstance(output, str) else repr(output) + raise ZenroomServiceError( + f"Zenroom script {script_name} did not return a JSON object.\n" + f"Output: {out_preview[:800]}\n" + f"Logs: {str(log_text)[:2000]}" + ) + def _pick_owner_block(self, res: Dict[str, Any], my_name: str, ctx: str) -> Dict[str, Any]: """ Zenroom often returns: { "": { ... } } @@ -126,7 +211,9 @@ class ZenroomServiceClient: 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}") + raise ZenroomServiceError( + f"Ambiguous {ctx} response (no key '{my_name}', len={len(res)}): {res!r}" + ) if not isinstance(owner, dict): raise ZenroomServiceError(f"Invalid {ctx} response structure: {res!r}") @@ -136,26 +223,12 @@ class ZenroomServiceClient: # 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) - res = self._post_data( + res = self._run_script( "Generate-a-keypair,-reading-identity-from-data", - {"myName": my_name}, + self.SCRIPT_GENERATE_KEYPAIR, + data={"myName": my_name}, ) owner = self._pick_owner_block(res, my_name, "keypair") @@ -166,15 +239,16 @@ class ZenroomServiceClient: private_key = keyring.get("ecdh") if not isinstance(private_key, str) or not private_key.strip(): - raise ZenroomServiceError(f"Invalid keypair response (missing keyring.ecdh): {res!r}") + raise ZenroomServiceError( + f"Invalid keypair response (missing keyring.ecdh): {res!r}" + ) out: Dict[str, Any] = { "my_name": my_name, "keyring": keyring, - "private_key": private_key, # convenience alias + "private_key": private_key, } - # 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 @@ -185,20 +259,12 @@ class ZenroomServiceClient: # 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( + res = self._run_script( "Generate-public-key", - {"keyring": keyring}, + self.SCRIPT_GENERATE_PUBLIC_KEY, + data={"keyring": keyring}, ) pub = res.get("ecdh_public_key") @@ -207,30 +273,24 @@ class ZenroomServiceClient: return pub # ------------------------------------------------------------------------- - # Service 3: Encrypt-a-message-with-the-password (symmetric) + # Service 3: Encrypt-a-message-with-the-password # ------------------------------------------------------------------------- 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( + res = self._run_script( "Encrypt-a-message-with-the-password", - {"header": header, "message": message, "password": shared_key}, + self.SCRIPT_SYMMETRIC_ENCRYPT, + data={"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}") + 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"): @@ -245,24 +305,16 @@ class ZenroomServiceClient: } # ------------------------------------------------------------------------- - # Service 4: Decrypt-the-message-with-the-password (symmetric) + # Service 4: Decrypt-the-message-with-the-password # ------------------------------------------------------------------------- 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( + res = self._run_script( "Decrypt-the-message-with-the-password", - {"secret_message": secret_message, "password": shared_key}, + self.SCRIPT_SYMMETRIC_DECRYPT, + data={"secret_message": secret_message, "password": shared_key}, ) txt = res.get("textDecrypted") @@ -281,21 +333,17 @@ class ZenroomServiceClient: 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) + 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( + res = self._run_script( "Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography", - { + self.SCRIPT_ASYMMETRIC_ENCRYPT, + data={ "reciever": {"public_key": receiver_public_key}, "sender": {"keyring": sender_keyring}, "header": header, @@ -305,7 +353,9 @@ class ZenroomServiceClient: sec = res.get("secret") if not isinstance(sec, dict): - raise ZenroomServiceError(f"Invalid asymmetric encrypt response (missing secret): {res!r}") + 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"): @@ -329,20 +379,14 @@ class ZenroomServiceClient: 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( + res = self._run_script( "Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography", - { + self.SCRIPT_ASYMMETRIC_DECRYPT, + data={ "sender": {"public_key": sender_public_key}, "reciever": {"keyring": receiver_keyring}, "secret": secret, @@ -361,31 +405,50 @@ class ZenroomServiceClient: # ------------------------------------------------------------------------- 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": {...}}}} + Signs exactly one string field from `objects`. - Response echoes fields and adds "<field>.signature": {"r": "...", "s": "..."}. - Returns response as-is (validated to contain at least one signature). + Example: + sign_objects(objects={"myMessage": "hello"}, signer_keyring=...) + + Returns e.g.: + { + "myMessage": "hello", + "myMessage.signature": {"r": "...", "s": "..."} + } """ objects = self._require_dict("objects", objects) signer_keyring = self._require_dict("signer_keyring", signer_keyring) - res = self._post_data( + if len(objects) != 1: + raise ZenroomServiceError( + f"sign_objects currently supports exactly one field, got keys={list(objects.keys())!r}" + ) + + field_name, field_value = next(iter(objects.items())) + field_name = self._require_safe_field_name(field_name) + field_value = self._require_non_empty_str(field_name, field_value) + + script = self.SCRIPT_SIGN_TEMPLATE.format(field_name=field_name) + + res = self._run_script( "Sign-objects-using-asymmetric-cryptography", - {"mySecretStuff": objects, "signer": {"keyring": signer_keyring}}, + script, + data={ + "mySecretStuff": {field_name: field_value}, + "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}") + sig_key = f"{field_name}.signature" + sig = res.get(sig_key) - 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}") + if not isinstance(sig, dict): + raise ZenroomServiceError(f"Invalid signature object for {sig_key}: {res!r}") + if not isinstance(sig.get("r"), str) or not isinstance(sig.get("s"), str): + raise ZenroomServiceError(f"Invalid signature fields for {sig_key}: {sig!r}") + + if not isinstance(res.get(field_name), str): + raise ZenroomServiceError(f"Missing signed field {field_name!r} in response: {res!r}") return res @@ -400,43 +463,39 @@ class ZenroomServiceClient: 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_field = self._require_safe_field_name(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) + script = self.SCRIPT_VERIFY_TEMPLATE.format(field_name=message_field) + 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) + res = self._run_script( + "Verify-asymmetric-cryptography-signature", + script, + data=payload, + ) out = res.get("output") - if not isinstance(out, list) or not out: - raise ZenroomServiceError(f"Invalid verify response: {res!r}") + if isinstance(out, list) and out: + return True - # We accept any non-empty success output, but the canonical string is: - # "Zenroom_certifies_that_signature_is_correct!" - return True + # Some Zenroom variants may print the success string directly as a named field + # or return the verified message only. If execution succeeded and no exception + # was raised, we still treat that as success when the original message is present. + if res.get(message_field) == message_value: + return True + + raise ZenroomServiceError(f"Invalid verify response: {res!r}") # ------------------------------------------------------------------------- - # Backward-compatible alias names (used by existing live tests / older code) + # Backward-compatible alias names # ------------------------------------------------------------------------- def generate_a_keypair_reading_identity_from_data(self, my_name: str) -> Dict[str, Any]: return self.generate_keypair(my_name) @@ -445,7 +504,5 @@ class ZenroomServiceClient: 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/test.py b/test.py new file mode 100644 index 0000000..f7bc05a --- /dev/null +++ b/test.py @@ -0,0 +1,18 @@ +from zenroom import zenroom + +contract = """Scenario ecdh: Create a ecdh key +Given that I am known as 'Alice' +When I create the ecdh key +Then print the 'keyring' +""" + +result = zenroom.zencode_exec(contract) + +print("OUTPUT:") +print(result.output) + +print("LOGS:") +print(result.logs) + +print("PARSED RESULT:") +print(result.result) diff --git a/test2.py b/test2.py new file mode 100644 index 0000000..ffee78b --- /dev/null +++ b/test2.py @@ -0,0 +1,9 @@ +from ca_core.crypto.zenroom_service_client import ZenroomServiceClient + +client = ZenroomServiceClient() + +kp = client.generate_keypair("Alice") +print("KEYPAIR:", kp) + +pub = client.generate_public_key(kp["keyring"]) +print("PUBLIC:", pub) diff --git a/test3.py b/test3.py new file mode 100644 index 0000000..bc0a178 --- /dev/null +++ b/test3.py @@ -0,0 +1,20 @@ +from ca_core.crypto.zenroom_service_client import ZenroomServiceClient + +client = ZenroomServiceClient() + +kp = client.generate_keypair("Alice") +pub = client.generate_public_key(kp["keyring"]) + +signed = client.sign_objects( + objects={"myMessage": "hello world"}, + signer_keyring=kp["keyring"], +) +print(signed) + +ok = client.verify_signature( + message_field="myMessage", + message_value=signed["myMessage"], + signature=signed["myMessage.signature"], + signer_public_key=pub, +) +print(ok) diff --git a/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc b/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc index 0fd43fb..e9f1778 100644 Binary files a/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc and b/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc differ diff --git a/tests/create_testdata.sql b/tests/create_testdata.sql deleted file mode 100644 index e25deff..0000000 --- a/tests/create_testdata.sql +++ /dev/null @@ -1,18 +0,0 @@ -truncate table entity; -truncate table group_member; -truncate table property; - -insert into entity (name,group_p,geo_offset,public_key) -values -('Svend',false,2355,'35AB456' ), -('Knud',false,4442,'36546AA'), -('Konger',true,3456, '87432CA'); - - -insert into group_member(group_id,person_id) -values -(3,1); - -insert into property(id,property_name) -values -(2,'aaa'); diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/integration/__pycache__/__init__.cpython-313.pyc b/tests/integration/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 871338f..0000000 Binary files a/tests/integration/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/tests/integration/__pycache__/test_integration_zenroom_docker.cpython-313.pyc b/tests/integration/__pycache__/test_integration_zenroom_docker.cpython-313.pyc deleted file mode 100644 index 6333830..0000000 Binary files a/tests/integration/__pycache__/test_integration_zenroom_docker.cpython-313.pyc and /dev/null differ diff --git a/tests/integration/__pycache__/test_zenroom_live.cpython-313.pyc b/tests/integration/__pycache__/test_zenroom_live.cpython-313.pyc deleted file mode 100644 index cc87de1..0000000 Binary files a/tests/integration/__pycache__/test_zenroom_live.cpython-313.pyc and /dev/null differ 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 deleted file mode 100644 index 0d0714b..0000000 Binary files a/tests/integration/__pycache__/test_zenroom_service_client_integration.cpython-313.pyc and /dev/null differ diff --git a/tests/integration/test_integration_zenroom_docker.py b/tests/integration/test_integration_zenroom_docker.py deleted file mode 100644 index f453d2c..0000000 --- a/tests/integration/test_integration_zenroom_docker.py +++ /dev/null @@ -1,88 +0,0 @@ -import os -import sys -import unittest -import subprocess -from pathlib import Path - -# Make ca_core importable as the module root (so `import crypto...` works) -code_path = Path(__file__).parents[1] / "ca_core" -sys.path.insert(0, str(code_path)) - -from ca_core.crypto.zenroom_client import ZenroomDockerClient, ZenroomError - - -def _docker_ok(): - """Return (ok, reason).""" - try: - p = subprocess.run( - ["docker", "version"], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - timeout=10, - check=False, - ) - except FileNotFoundError: - return False, "docker CLI not found" - except Exception as e: - return False, f"docker check failed: {e}" - - if p.returncode != 0: - out = (p.stdout or "").strip() - return False, f"docker not usable: {out}" - return True, "" - - -def _image_exists(image: str): - """Return (ok, reason).""" - try: - p = subprocess.run( - ["docker", "image", "inspect", image], - stdout=subprocess.DEVNULL, - stderr=subprocess.STDOUT, - text=True, - timeout=10, - check=False, - ) - except Exception as e: - return False, f"docker image inspect failed: {e}" - - if p.returncode != 0: - return False, f"docker image not found locally: {image}" - return True, "" - - -class TestZenroomDockerIntegration(unittest.TestCase): - """Integration tests for ZenroomDockerClient. - - Enable by setting: - ZENROOM_DOCKER_INTEGRATION=1 - - Image selection: - ZENROOM_IMAGE (default: zenroom) - """ - - @classmethod - def setUpClass(cls): - if not os.getenv("ZENROOM_DOCKER_INTEGRATION"): - raise unittest.SkipTest("Docker integration disabled (set ZENROOM_DOCKER_INTEGRATION=1)") - - ok, reason = _docker_ok() - if not ok: - raise unittest.SkipTest(reason) - - cls.image = os.getenv("ZENROOM_IMAGE", "zenroom") - - ok, reason = _image_exists(cls.image) - if not ok: - raise unittest.SkipTest(reason + " (build it or set ZENROOM_IMAGE)") - - def test_basic_execution(self): - client = ZenroomDockerClient(image=self.image) - out = client.run("print('hello')") - self.assertIn("hello", str(out)) - - def test_nonzero_exit_raises(self): - client = ZenroomDockerClient(image=self.image) - with self.assertRaises(ZenroomError): - client.run("THIS IS NOT VALID ZENCODE") diff --git a/tests/integration/test_zenroom_live.py b/tests/integration/test_zenroom_live.py deleted file mode 100644 index 114528e..0000000 --- a/tests/integration/test_zenroom_live.py +++ /dev/null @@ -1,71 +0,0 @@ -import os -import unittest -import sys -from pathlib import Path - -# Import from ca_core -code_path = Path(__file__).parent.parent.parent / "ca_core" -sys.path.insert(0, str(code_path)) - -from ca_core.crypto.zenroom_service_client import ZenroomServiceClient - - -def _live_enabled() -> bool: - return os.environ.get("RUN_LIVE_ZENROOM", "").strip().lower() in {"1", "true", "yes"} - - -@unittest.skipUnless(_live_enabled(), "Set RUN_LIVE_ZENROOM=1 to run live Zenroom service smoke tests") -class TestZenroomLiveServices(unittest.TestCase): - - @classmethod - def setUpClass(cls): - base_url = os.environ.get("ZENROOM_BASE_URL", "http://localhost:3300").strip() - api_prefix = os.environ.get("ZENROOM_API_PREFIX", "/api").strip() - timeout_s = int(os.environ.get("ZENROOM_TIMEOUT_S", "20")) - - cls.client = ZenroomServiceClient( - base_url=base_url, - api_prefix=api_prefix, - timeout_s=timeout_s, - ) - - def test_end_to_end_8_calls(self): - sender_kp = self.client.generate_keypair("LiveUser123456") - sender_pub = self.client.generate_public_key(sender_kp["keyring"]) - - plaintext = "Dear Bob, your name is too short, goodbye - Alice." - sm = self.client.symmetric_encrypt( - header="A very important secret", - message=plaintext, - shared_key="myVerySecretPassword", - ) - pt = self.client.symmetric_decrypt(secret_message=sm, shared_key="myVerySecretPassword") - self.assertEqual(pt, plaintext) - - secret = self.client.asymmetric_encrypt( - receiver_public_key=sender_pub, - sender_keyring=sender_kp["keyring"], - message="Hello from live test", - header="Live header", - ) - dec = self.client.asymmetric_decrypt( - sender_public_key=sender_pub, - receiver_keyring=sender_kp["keyring"], - secret=secret, - ) - self.assertEqual(dec["header"], "Live header") - self.assertEqual(dec["text"], "Hello from live test") - - signed = self.client.sign_objects( - signer_keyring=sender_kp["keyring"], - objects={"myMessage": "Signed live message"}, - ) - sig = signed["myMessage.signature"] - - ok = self.client.verify_signature( - message_field="myMessage", - message_value=signed["myMessage"], - signature={"r": sig["r"], "s": sig["s"]}, - signer_public_key=sender_pub, - ) - self.assertTrue(ok) diff --git a/tests/integration/test_zenroom_service_client_integration.py b/tests/integration/test_zenroom_service_client_integration.py deleted file mode 100644 index 9380f84..0000000 --- a/tests/integration/test_zenroom_service_client_integration.py +++ /dev/null @@ -1,89 +0,0 @@ -import os -import unittest -import sys -from pathlib import Path - -# Import from ca_core -code_path = Path(__file__).parents[2] / "ca_core" -sys.path.insert(0, str(code_path)) - -from ca_core.crypto.zenroom_service_client import ZenroomServiceClient - - -class TestZenroomServiceClientIntegration(unittest.TestCase): - """Service integration: runs against a live RESTroom/Zenroom HTTP service. - - Enable by setting: - ZENROOM_BASE_URL=http://localhost:3300 - """ - - @unittest.skipUnless(os.getenv("ZENROOM_BASE_URL"), "No ZENROOM_BASE_URL set") - def test_end_to_end_smoke_8_calls(self): - """Hits exactly these 8 endpoints once each (in typical service logs): - 1) Generate-a-keypair,-reading-identity-from-data - 2) Generate-public-key - 3) Encrypt-a-message-with-the-password - 4) Decrypt-the-message-with-the-password - 5) Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography - 6) Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography - 7) Sign-objects-using-asymmetric-cryptography - 8) Verify-asymmetric-cryptography-signature - - Note: To keep it to 8 calls, we reuse the same identity as both sender and receiver - for the asymmetric encrypt/decrypt roundtrip. - """ - client = ZenroomServiceClient(base_url=os.environ["ZENROOM_BASE_URL"]) - - # 1) keypair (private) -> 2) public key - sender_kp = client.generate_keypair("IntegrationUser123456") - self.assertIn("keyring", sender_kp) - self.assertIn("private_key", sender_kp) - - sender_pub = client.generate_public_key(sender_kp["keyring"]) - self.assertIsInstance(sender_pub, str) - self.assertTrue(sender_pub.strip()) - - # 3) symmetric encrypt -> 4) symmetric decrypt - 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) - - # 5) asymmetric encrypt (receiver pub == sender pub to avoid extra keypair/public-key calls) - secret = client.asymmetric_encrypt( - receiver_public_key=sender_pub, - sender_keyring=sender_kp["keyring"], - message="Hello from integration test", - header="Integration header", - ) - - # 6) asymmetric decrypt (sender public key is sender_pub; receiver private key == sender private key) - dec = client.asymmetric_decrypt( - sender_public_key=sender_pub, - receiver_keyring=sender_kp["keyring"], - secret=secret, - ) - self.assertEqual(dec["header"], "Integration header") - self.assertEqual(dec["text"], "Hello from integration test") - - # 7) sign -> 8) verify - signed = client.sign_objects( - signer_keyring=sender_kp["keyring"], - objects={"myMessage": "Signed integration message"}, - ) - self.assertIn("myMessage.signature", signed) - sig = signed["myMessage.signature"] - self.assertIn("r", sig) - self.assertIn("s", sig) - - ok = client.verify_signature( - message_field="myMessage", - message_value=signed["myMessage"], - signature={"r": sig["r"], "s": sig["s"]}, - signer_public_key=sender_pub, - ) - self.assertTrue(ok) diff --git a/tests/test_zenroom_client.py b/tests/test_zenroom_client.py deleted file mode 100644 index 8043ad6..0000000 --- a/tests/test_zenroom_client.py +++ /dev/null @@ -1,90 +0,0 @@ -import unittest -import sys -from pathlib import Path -from unittest import mock - -# Allow imports from ca_core (same pattern as existing tests) -code_path = Path(__file__).parent.parent / "ca_core" -sys.path.insert(0, str(code_path)) - -from ca_core.crypto.zenroom_client import ZenroomDockerClient, ZenroomError - - -class TestZenroomDockerClient(unittest.TestCase): - def _fake_completed(self, returncode=0, stdout="", stderr=""): - cp = mock.Mock() - cp.returncode = returncode - cp.stdout = stdout - cp.stderr = stderr - return cp - - @mock.patch("crypto.zenroom_client.subprocess.run") - def test_run_builds_expected_docker_command(self, m_run): - m_run.return_value = self._fake_completed(stdout='{"ok": true}') - client = ZenroomDockerClient(image="zenroom/zenroom:latest") - - # Patch temp dir so we can assert paths deterministically - with mock.patch("crypto.zenroom_client.tempfile.TemporaryDirectory") as m_td: - m_td.return_value.__enter__.return_value = "/tmp/zenroom_test" - m_td.return_value.__exit__.return_value = False - - res = client.run("print('hi')", data={"a": 1}, keys={"k": "v"}, conf={"c": 2}) - - self.assertEqual(res, {"ok": True}) - - args, kwargs = m_run.call_args - cmd = args[0] - self.assertIn("docker", cmd[0]) - self.assertIn("run", cmd) - self.assertIn("zenroom/zenroom:latest", cmd) - - # Mount and workdir - self.assertIn("-v", cmd) - self.assertIn("/tmp/zenroom_test:/work", cmd) - self.assertIn("-w", cmd) - self.assertIn("/work", cmd) - - # Zenroom base args - self.assertIn("zenroom", cmd) - self.assertIn("-z", cmd) - - # Input files flags should be present - self.assertIn("-a", cmd) - self.assertIn("/work/data.json", cmd) - self.assertIn("-k", cmd) - self.assertIn("/work/keys.json", cmd) - self.assertIn("-c", cmd) - self.assertIn("/work/conf.json", cmd) - - # Script at end - self.assertEqual(cmd[-1], "/work/script.zen") - - # subprocess.run called with capture_output/text - self.assertTrue(kwargs.get("capture_output")) - self.assertTrue(kwargs.get("text")) - - @mock.patch("crypto.zenroom_client.subprocess.run") - def test_run_returns_raw_stdout_when_not_json(self, m_run): - m_run.return_value = self._fake_completed(stdout="hello") - client = ZenroomDockerClient() - with mock.patch("crypto.zenroom_client.tempfile.TemporaryDirectory") as m_td: - m_td.return_value.__enter__.return_value = "/tmp/zenroom_test" - m_td.return_value.__exit__.return_value = False - out = client.run("print('hi')") - self.assertEqual(out, "hello") - - @mock.patch("crypto.zenroom_client.subprocess.run") - def test_run_raises_on_nonzero_exit(self, m_run): - m_run.return_value = self._fake_completed(returncode=1, stderr="boom") - client = ZenroomDockerClient() - with mock.patch("crypto.zenroom_client.tempfile.TemporaryDirectory") as m_td: - m_td.return_value.__enter__.return_value = "/tmp/zenroom_test" - m_td.return_value.__exit__.return_value = False - with self.assertRaises(ZenroomError) as ctx: - client.run("print('hi')") - self.assertIn("boom", str(ctx.exception)) - - def test_run_requires_non_empty_script(self): - client = ZenroomDockerClient() - with self.assertRaises(ValueError): - client.run(" ") diff --git a/tests/test_zenroom_service_client.py b/tests/test_zenroom_service_client.py index 236a36d..84cb7d5 100644 --- a/tests/test_zenroom_service_client.py +++ b/tests/test_zenroom_service_client.py @@ -1,224 +1,162 @@ -import json import unittest -from unittest import mock -import sys -from pathlib import Path -code_path = Path(__file__).parents[1] / "ca_core" -sys.path.insert(0, str(code_path)) - -from ca_core.crypto.zenroom_service_client import ZenroomServiceClient, ZenroomServiceError - - -class _FakeHTTPResponse: - def __init__(self, body: bytes): - self._body = body - - def read(self): - return self._body - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - return False +from ca_core.crypto.zenroom_service_client import ZenroomServiceClient class TestZenroomServiceClient(unittest.TestCase): + def setUp(self): + self.client = ZenroomServiceClient() - @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") - def test_generate_keypair_returns_keyring_and_private_key(self, m_urlopen): - payload = { - "User123456": {"keyring": {"ecdh": "PRIVKEY"}} - } - m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8")) + def test_1_generate_keypair(self): + result = self.client.generate_keypair("Alice") - client = ZenroomServiceClient(base_url="http://localhost:3300") - res = client.generate_keypair("User123456") + self.assertEqual(result["my_name"], "Alice") + self.assertIn("keyring", result) + self.assertIsInstance(result["keyring"], dict) + self.assertIn("ecdh", result["keyring"]) + self.assertIsInstance(result["keyring"]["ecdh"], str) + self.assertTrue(result["keyring"]["ecdh"]) + self.assertEqual(result["private_key"], result["keyring"]["ecdh"]) - self.assertEqual(res["my_name"], "User123456") - self.assertEqual(res["private_key"], "PRIVKEY") - self.assertEqual(res["keyring"], {"ecdh": "PRIVKEY"}) - self.assertNotIn("public_key", res) + def test_2_generate_public_key(self): + keypair = self.client.generate_keypair("Alice") + public_key = self.client.generate_public_key(keypair["keyring"]) - 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": "User123456"}}) + self.assertIsInstance(public_key, str) + self.assertTrue(public_key) - @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") - 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") - pub = client.generate_public_key({"ecdh": "PRIVKEY"}) - self.assertEqual(pub, "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"}}, + def test_3_symmetric_encrypt(self): + result = self.client.symmetric_encrypt( + header="test-header", + message="hello symmetric crypto", + shared_key="correct horse battery staple", ) - @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")) + self.assertIsInstance(result, dict) + self.assertIn("checksum", result) + self.assertIn("header", result) + self.assertIn("iv", result) + self.assertIn("text", result) - client = ZenroomServiceClient(base_url="http://localhost:3300") - txt = client.symmetric_decrypt(secret_message={"iv": "x"}, shared_key="k") - self.assertEqual(txt, "PLAINTEXT") + self.assertIsInstance(result["checksum"], str) + self.assertIsInstance(result["header"], str) + self.assertIsInstance(result["iv"], str) + self.assertIsInstance(result["text"], str) - 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"}}) + self.assertTrue(result["checksum"]) + self.assertTrue(result["header"]) + self.assertTrue(result["iv"]) + self.assertTrue(result["text"]) - @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")) + def test_4_symmetric_decrypt(self): + plaintext = "hello symmetric crypto" + password = "correct horse battery staple" - 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", - } - }, + encrypted = self.client.symmetric_encrypt( + header="test-header", + message=plaintext, + shared_key=password, ) - @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"}, - } - }, + decrypted = self.client.symmetric_decrypt( + secret_message=encrypted, + shared_key=password, ) - @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")) + self.assertEqual(decrypted, plaintext) - client = ZenroomServiceClient(base_url="http://localhost:3300") - res = client.sign_objects(objects={"myMessage": "hello"}, signer_keyring={"ecdh": "PRIV"}) + def test_5_asymmetric_encrypt(self): + alice = self.client.generate_keypair("Alice") + bob = self.client.generate_keypair("Bob") + bob_public_key = self.client.generate_public_key(bob["keyring"]) - 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"}}}}, + result = self.client.asymmetric_encrypt( + receiver_public_key=bob_public_key, + sender_keyring=alice["keyring"], + header="asym-header", + message="hello bob", ) - @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")) + self.assertIsInstance(result, dict) + self.assertIn("checksum", result) + self.assertIn("header", result) + self.assertIn("iv", result) + self.assertIn("text", result) - client = ZenroomServiceClient(base_url="http://localhost:3300") - ok = client.verify_signature( + self.assertIsInstance(result["checksum"], str) + self.assertIsInstance(result["header"], str) + self.assertIsInstance(result["iv"], str) + self.assertIsInstance(result["text"], str) + + self.assertTrue(result["checksum"]) + self.assertTrue(result["header"]) + self.assertTrue(result["iv"]) + self.assertTrue(result["text"]) + + def test_6_asymmetric_decrypt(self): + alice = self.client.generate_keypair("Alice") + bob = self.client.generate_keypair("Bob") + + alice_public_key = self.client.generate_public_key(alice["keyring"]) + bob_public_key = self.client.generate_public_key(bob["keyring"]) + + encrypted = self.client.asymmetric_encrypt( + receiver_public_key=bob_public_key, + sender_keyring=alice["keyring"], + header="asym-header", + message="hello bob", + ) + + decrypted = self.client.asymmetric_decrypt( + sender_public_key=alice_public_key, + receiver_keyring=bob["keyring"], + secret=encrypted, + ) + + self.assertIsInstance(decrypted, dict) + self.assertEqual(decrypted["header"], "asym-header") + self.assertEqual(decrypted["text"], "hello bob") + + def test_7_sign_objects(self): + alice = self.client.generate_keypair("Alice") + + result = self.client.sign_objects( + objects={"myMessage": "hello signed world"}, + signer_keyring=alice["keyring"], + ) + + self.assertIsInstance(result, dict) + self.assertIn("myMessage", result) + self.assertIn("myMessage.signature", result) + self.assertEqual(result["myMessage"], "hello signed world") + + signature = result["myMessage.signature"] + self.assertIsInstance(signature, dict) + self.assertIn("r", signature) + self.assertIn("s", signature) + self.assertIsInstance(signature["r"], str) + self.assertIsInstance(signature["s"], str) + self.assertTrue(signature["r"]) + self.assertTrue(signature["s"]) + + def test_8_verify_signature(self): + alice = self.client.generate_keypair("Alice") + alice_public_key = self.client.generate_public_key(alice["keyring"]) + + signed = self.client.sign_objects( + objects={"myMessage": "hello signed world"}, + signer_keyring=alice["keyring"], + ) + + verified = self.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"}}}, + message_value=signed["myMessage"], + signature=signed["myMessage.signature"], + signer_public_key=alice_public_key, ) - @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")) + self.assertTrue(verified) - 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", - ) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_zenroom_service_client_clean.py b/tests/test_zenroom_service_client_clean.py deleted file mode 100644 index 7fb26b2..0000000 --- a/tests/test_zenroom_service_client_clean.py +++ /dev/null @@ -1,55 +0,0 @@ -import json -import unittest -from unittest import mock -import sys -from pathlib import Path - -code_path = Path(__file__).parents[1] / "ca_core" -sys.path.insert(0, str(code_path)) - -from ca_core.crypto.zenroom_service_client import ZenroomServiceClient - - -class _FakeHTTPResponse: - def __init__(self, body: bytes): - self._body = body - - def read(self): - return self._body - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, tb): - return False - - -class TestZenroomServiceClient(unittest.TestCase): - - @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") - def test_generate_keypair_unpacks_keys(self, m_urlopen): - - payload = { - "Owner": { - "ecdh_public_key": "PUBKEY", - "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("User123") - - self.assertEqual(res["public_key"], "PUBKEY") - self.assertEqual(res["private_key"], "PRIVKEY") - - 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" - ) - )