local zenroom integration
This commit is contained in:
parent
c6d4ce1906
commit
4578d27433
|
|
@ -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
|
||||
|
|
@ -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.
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
Binary file not shown.
|
|
@ -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');
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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(" ")
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
)
|
||||
Loading…
Reference in New Issue