local zenroom integration

This commit is contained in:
Morten V. Christiansen 2026-03-11 07:12:00 +01:00
parent c6d4ce1906
commit 4578d27433
24 changed files with 785 additions and 1278 deletions

View File

@ -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

361
PROJECT_CONTEXT.rtf Normal file
View File

@ -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 }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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 <data.json> -k <keys.json> -c <conf.json> <script.zen>
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

View File

@ -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: { "<identity>": { ... } }
@ -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": "<identity>"}}
Observed response:
{ "<identity>": { "keyring": { "ecdh": "<private_b64>" } } }
Return (normalized, plus backward-compatible fields):
{
"my_name": "<identity>",
"keyring": {"ecdh": "<private_b64>"},
"private_key": "<private_b64>",
# "public_key": "<public_b64>" 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": "<b64>"}
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": "<plaintext>"}
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}

18
test.py Normal file
View File

@ -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)

9
test2.py Normal file
View File

@ -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)

20
test3.py Normal file
View File

@ -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)

View File

@ -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');

View File

@ -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")

View File

@ -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)

View File

@ -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)

View File

@ -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(" ")

View File

@ -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()

View File

@ -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"
)
)