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 json
|
||||||
import urllib.request
|
import re
|
||||||
import urllib.error
|
|
||||||
from typing import Any, Dict, Optional, Tuple
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
|
from zenroom import zenroom
|
||||||
|
|
||||||
|
|
||||||
class ZenroomServiceError(RuntimeError):
|
class ZenroomServiceError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZenroomServiceClient:
|
class ZenroomServiceClient:
|
||||||
|
"""
|
||||||
|
Local Zenroom client using the installed `zenroom` Python wrapper.
|
||||||
|
|
||||||
def __init__(
|
This preserves the public API of the old HTTP/Docker-backed client as much
|
||||||
self,
|
as possible, so callers should not need changes.
|
||||||
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()
|
|
||||||
|
|
||||||
if self.api_prefix in {"", "/"}:
|
SCRIPT_GENERATE_KEYPAIR = """Scenario 'ecdh': Create the keypair from a name passed from data/keys
|
||||||
self.api_prefix = ""
|
|
||||||
elif not self.api_prefix.startswith("/"):
|
|
||||||
self.api_prefix = "/" + self.api_prefix
|
|
||||||
|
|
||||||
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:
|
# Here we generate and print the keypair
|
||||||
path = "/" + path.lstrip("/")
|
When I create the ecdh key
|
||||||
return f"{self.base_url}{self.api_prefix}{path}"
|
Then print my 'keyring'
|
||||||
|
"""
|
||||||
|
|
||||||
def _request_json(
|
SCRIPT_GENERATE_PUBLIC_KEY = """# Loading scenarios
|
||||||
self,
|
Scenario 'ecdh': Create the public key
|
||||||
method: str,
|
|
||||||
path: str,
|
|
||||||
payload: Optional[Dict[str, Any]] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
|
|
||||||
url = self._make_url(path)
|
# Loading the private keys
|
||||||
data = None
|
Given I have the 'keyring'
|
||||||
headers = {"Accept": "application/json"}
|
|
||||||
|
|
||||||
if payload is not None:
|
# Generating the public keys
|
||||||
data = json.dumps(payload).encode("utf-8")
|
When I create the ecdh public key
|
||||||
headers["Content-Type"] = "application/json"
|
|
||||||
|
|
||||||
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:
|
SCRIPT_SYMMETRIC_ENCRYPT = """Scenario 'ecdh': Encrypt a message with the password
|
||||||
with urllib.request.urlopen(req, timeout=self.timeout_s) as resp:
|
Given that I have a 'string' named 'password'
|
||||||
raw = resp.read()
|
Given that I have a 'string' named 'header'
|
||||||
text = raw.decode("utf-8")
|
Given that I have a 'string' named 'message'
|
||||||
except urllib.error.HTTPError as e:
|
When I encrypt the secret message 'message' with 'password'
|
||||||
body = ""
|
Then print the 'secret message'
|
||||||
try:
|
"""
|
||||||
body = e.read().decode("utf-8")
|
|
||||||
except Exception:
|
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'
|
||||||
|
"""
|
||||||
|
|
||||||
|
SCRIPT_ASYMMETRIC_ENCRYPT = """Scenario 'ecdh': Alice encrypts a message for Bob
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
When I encrypt the secret message of 'message' for 'reciever'
|
||||||
|
When I rename the 'secret message' to 'secret'
|
||||||
|
|
||||||
|
Then print the 'secret'
|
||||||
|
"""
|
||||||
|
|
||||||
|
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'
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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'
|
||||||
|
|
||||||
|
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
|
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
|
|
||||||
|
|
||||||
text = text.strip()
|
|
||||||
if not text:
|
|
||||||
raise ZenroomServiceError(f"Empty response from {url}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
parsed = json.loads(text)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
raise ZenroomServiceError(f"Non-JSON response from {url}: {text[:200]}") from e
|
|
||||||
|
|
||||||
if not isinstance(parsed, dict):
|
|
||||||
raise ZenroomServiceError(f"Expected JSON object from {url}")
|
|
||||||
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
return self._request_json("POST", path, payload)
|
|
||||||
|
|
||||||
def _post_data(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
# Per your rule: if "keys" is empty, omit it entirely.
|
|
||||||
# All your services in this round only need {"data": ...}
|
|
||||||
res = self._post(path, {"data": data})
|
|
||||||
|
|
||||||
# RESTroom convention: on failure you get zenroom_errors and/or exception
|
|
||||||
if "zenroom_errors" in res or "exception" in res:
|
|
||||||
exc = res.get("exception", "")
|
|
||||||
ze = res.get("zenroom_errors")
|
|
||||||
logs = ""
|
|
||||||
if isinstance(ze, dict):
|
|
||||||
logs = str(ze.get("logs", ""))[:800]
|
|
||||||
raise ZenroomServiceError(f"Zenroom error from {path}: {exc or logs or 'unknown error'}")
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _require_non_empty_str(name: str, value: str) -> str:
|
def _require_non_empty_str(name: str, value: str) -> str:
|
||||||
|
|
@ -116,6 +139,68 @@ class ZenroomServiceClient:
|
||||||
if missing:
|
if missing:
|
||||||
raise ZenroomServiceError(f"Missing {missing} in {ctx}: {d!r}")
|
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]:
|
def _pick_owner_block(self, res: Dict[str, Any], my_name: str, ctx: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Zenroom often returns: { "<identity>": { ... } }
|
Zenroom often returns: { "<identity>": { ... } }
|
||||||
|
|
@ -126,7 +211,9 @@ class ZenroomServiceClient:
|
||||||
elif len(res) == 1:
|
elif len(res) == 1:
|
||||||
owner = next(iter(res.values()))
|
owner = next(iter(res.values()))
|
||||||
else:
|
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):
|
if not isinstance(owner, dict):
|
||||||
raise ZenroomServiceError(f"Invalid {ctx} response structure: {res!r}")
|
raise ZenroomServiceError(f"Invalid {ctx} response structure: {res!r}")
|
||||||
|
|
@ -136,26 +223,12 @@ class ZenroomServiceClient:
|
||||||
# Service 1: Generate-a-keypair,-reading-identity-from-data
|
# Service 1: Generate-a-keypair,-reading-identity-from-data
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
def generate_keypair(self, my_name: str) -> Dict[str, Any]:
|
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)
|
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",
|
"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")
|
owner = self._pick_owner_block(res, my_name, "keypair")
|
||||||
|
|
@ -166,15 +239,16 @@ class ZenroomServiceClient:
|
||||||
|
|
||||||
private_key = keyring.get("ecdh")
|
private_key = keyring.get("ecdh")
|
||||||
if not isinstance(private_key, str) or not private_key.strip():
|
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] = {
|
out: Dict[str, Any] = {
|
||||||
"my_name": my_name,
|
"my_name": my_name,
|
||||||
"keyring": keyring,
|
"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")
|
public_key = owner.get("ecdh_public_key")
|
||||||
if isinstance(public_key, str) and public_key.strip():
|
if isinstance(public_key, str) and public_key.strip():
|
||||||
out["public_key"] = public_key
|
out["public_key"] = public_key
|
||||||
|
|
@ -185,20 +259,12 @@ class ZenroomServiceClient:
|
||||||
# Service 2: Generate-public-key
|
# Service 2: Generate-public-key
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
def generate_public_key(self, keyring: Dict[str, Any]) -> str:
|
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)
|
keyring = self._require_dict("keyring", keyring)
|
||||||
|
|
||||||
res = self._post_data(
|
res = self._run_script(
|
||||||
"Generate-public-key",
|
"Generate-public-key",
|
||||||
{"keyring": keyring},
|
self.SCRIPT_GENERATE_PUBLIC_KEY,
|
||||||
|
data={"keyring": keyring},
|
||||||
)
|
)
|
||||||
|
|
||||||
pub = res.get("ecdh_public_key")
|
pub = res.get("ecdh_public_key")
|
||||||
|
|
@ -207,30 +273,24 @@ class ZenroomServiceClient:
|
||||||
return pub
|
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]:
|
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)
|
header = self._require_non_empty_str("header", header)
|
||||||
message = self._require_non_empty_str("message", message)
|
message = self._require_non_empty_str("message", message)
|
||||||
shared_key = self._require_non_empty_str("shared_key", shared_key)
|
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",
|
"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")
|
sm = res.get("secret_message")
|
||||||
if not isinstance(sm, dict):
|
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")
|
self._require_keys(sm, required=("checksum", "header", "iv", "text"), ctx="secret_message")
|
||||||
for k in ("checksum", "header", "iv", "text"):
|
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:
|
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)
|
secret_message = self._require_dict("secret_message", secret_message)
|
||||||
shared_key = self._require_non_empty_str("shared_key", shared_key)
|
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",
|
"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")
|
txt = res.get("textDecrypted")
|
||||||
|
|
@ -281,21 +333,17 @@ class ZenroomServiceClient:
|
||||||
header: str,
|
header: str,
|
||||||
message: str,
|
message: str,
|
||||||
) -> Dict[str, str]:
|
) -> Dict[str, str]:
|
||||||
"""
|
receiver_public_key = self._require_non_empty_str(
|
||||||
POST Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography
|
"receiver_public_key", receiver_public_key
|
||||||
|
)
|
||||||
Note: service expects 'reciever' spelling.
|
|
||||||
|
|
||||||
Returns inner 'secret' dict with checksum/header/iv/text.
|
|
||||||
"""
|
|
||||||
receiver_public_key = self._require_non_empty_str("receiver_public_key", receiver_public_key)
|
|
||||||
sender_keyring = self._require_dict("sender_keyring", sender_keyring)
|
sender_keyring = self._require_dict("sender_keyring", sender_keyring)
|
||||||
header = self._require_non_empty_str("header", header)
|
header = self._require_non_empty_str("header", header)
|
||||||
message = self._require_non_empty_str("message", message)
|
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",
|
"Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography",
|
||||||
{
|
self.SCRIPT_ASYMMETRIC_ENCRYPT,
|
||||||
|
data={
|
||||||
"reciever": {"public_key": receiver_public_key},
|
"reciever": {"public_key": receiver_public_key},
|
||||||
"sender": {"keyring": sender_keyring},
|
"sender": {"keyring": sender_keyring},
|
||||||
"header": header,
|
"header": header,
|
||||||
|
|
@ -305,7 +353,9 @@ class ZenroomServiceClient:
|
||||||
|
|
||||||
sec = res.get("secret")
|
sec = res.get("secret")
|
||||||
if not isinstance(sec, dict):
|
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")
|
self._require_keys(sec, required=("checksum", "header", "iv", "text"), ctx="secret")
|
||||||
for k in ("checksum", "header", "iv", "text"):
|
for k in ("checksum", "header", "iv", "text"):
|
||||||
|
|
@ -329,20 +379,14 @@ class ZenroomServiceClient:
|
||||||
receiver_keyring: Dict[str, Any],
|
receiver_keyring: Dict[str, Any],
|
||||||
secret: Dict[str, Any],
|
secret: Dict[str, Any],
|
||||||
) -> Dict[str, str]:
|
) -> 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)
|
sender_public_key = self._require_non_empty_str("sender_public_key", sender_public_key)
|
||||||
receiver_keyring = self._require_dict("receiver_keyring", receiver_keyring)
|
receiver_keyring = self._require_dict("receiver_keyring", receiver_keyring)
|
||||||
secret = self._require_dict("secret", secret)
|
secret = self._require_dict("secret", secret)
|
||||||
|
|
||||||
res = self._post_data(
|
res = self._run_script(
|
||||||
"Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography",
|
"Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography",
|
||||||
{
|
self.SCRIPT_ASYMMETRIC_DECRYPT,
|
||||||
|
data={
|
||||||
"sender": {"public_key": sender_public_key},
|
"sender": {"public_key": sender_public_key},
|
||||||
"reciever": {"keyring": receiver_keyring},
|
"reciever": {"keyring": receiver_keyring},
|
||||||
"secret": secret,
|
"secret": secret,
|
||||||
|
|
@ -361,31 +405,50 @@ class ZenroomServiceClient:
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
def sign_objects(self, *, objects: Dict[str, Any], signer_keyring: Dict[str, Any]) -> Dict[str, Any]:
|
def sign_objects(self, *, objects: Dict[str, Any], signer_keyring: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
POST Sign-objects-using-asymmetric-cryptography
|
Signs exactly one string field from `objects`.
|
||||||
body: {"data": {"mySecretStuff": {...}, "signer": {"keyring": {...}}}}
|
|
||||||
|
|
||||||
Response echoes fields and adds "<field>.signature": {"r": "...", "s": "..."}.
|
Example:
|
||||||
Returns response as-is (validated to contain at least one signature).
|
sign_objects(objects={"myMessage": "hello"}, signer_keyring=...)
|
||||||
|
|
||||||
|
Returns e.g.:
|
||||||
|
{
|
||||||
|
"myMessage": "hello",
|
||||||
|
"myMessage.signature": {"r": "...", "s": "..."}
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
objects = self._require_dict("objects", objects)
|
objects = self._require_dict("objects", objects)
|
||||||
signer_keyring = self._require_dict("signer_keyring", signer_keyring)
|
signer_keyring = self._require_dict("signer_keyring", signer_keyring)
|
||||||
|
|
||||||
res = self._post_data(
|
if len(objects) != 1:
|
||||||
"Sign-objects-using-asymmetric-cryptography",
|
raise ZenroomServiceError(
|
||||||
{"mySecretStuff": objects, "signer": {"keyring": signer_keyring}},
|
f"sign_objects currently supports exactly one field, got keys={list(objects.keys())!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate at least one "*.signature" and that each has r/s.
|
field_name, field_value = next(iter(objects.items()))
|
||||||
sig_keys = [k for k in res.keys() if isinstance(k, str) and k.endswith(".signature")]
|
field_name = self._require_safe_field_name(field_name)
|
||||||
if not sig_keys:
|
field_value = self._require_non_empty_str(field_name, field_value)
|
||||||
raise ZenroomServiceError(f"No signatures found in sign response: {res!r}")
|
|
||||||
|
script = self.SCRIPT_SIGN_TEMPLATE.format(field_name=field_name)
|
||||||
|
|
||||||
|
res = self._run_script(
|
||||||
|
"Sign-objects-using-asymmetric-cryptography",
|
||||||
|
script,
|
||||||
|
data={
|
||||||
|
"mySecretStuff": {field_name: field_value},
|
||||||
|
"signer": {"keyring": signer_keyring},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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):
|
if not isinstance(sig, dict):
|
||||||
raise ZenroomServiceError(f"Invalid signature object for {k}: {res!r}")
|
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):
|
if not isinstance(sig.get("r"), str) or not isinstance(sig.get("s"), str):
|
||||||
raise ZenroomServiceError(f"Invalid signature fields for {k}: {sig!r}")
|
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
|
return res
|
||||||
|
|
||||||
|
|
@ -400,43 +463,39 @@ class ZenroomServiceClient:
|
||||||
signature: Dict[str, Any],
|
signature: Dict[str, Any],
|
||||||
signer_public_key: str,
|
signer_public_key: str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
message_field = self._require_safe_field_name(message_field)
|
||||||
POST Verify-asymmetric-cryptography-signature
|
|
||||||
|
|
||||||
Input example uses dynamic field names like:
|
|
||||||
"myMessage": "...",
|
|
||||||
"myMessage.signature": {"r": "...", "s": "..."},
|
|
||||||
"signer": {"public_key": "..."}
|
|
||||||
|
|
||||||
On success, response includes:
|
|
||||||
{"output": ["Zenroom_certifies_that_signature_is_correct!"], ...}
|
|
||||||
|
|
||||||
On failure, RESTroom returns zenroom_errors/exception which _post_data raises.
|
|
||||||
Returns True on success.
|
|
||||||
"""
|
|
||||||
message_field = self._require_non_empty_str("message_field", message_field)
|
|
||||||
message_value = self._require_non_empty_str("message_value", message_value)
|
message_value = self._require_non_empty_str("message_value", message_value)
|
||||||
signature = self._require_dict("signature", signature)
|
signature = self._require_dict("signature", signature)
|
||||||
signer_public_key = self._require_non_empty_str("signer_public_key", signer_public_key)
|
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] = {
|
payload: Dict[str, Any] = {
|
||||||
message_field: message_value,
|
message_field: message_value,
|
||||||
f"{message_field}.signature": signature,
|
f"{message_field}.signature": signature,
|
||||||
"signer": {"public_key": signer_public_key},
|
"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")
|
out = res.get("output")
|
||||||
if not isinstance(out, list) or not out:
|
if isinstance(out, list) and out:
|
||||||
raise ZenroomServiceError(f"Invalid verify response: {res!r}")
|
|
||||||
|
|
||||||
# We accept any non-empty success output, but the canonical string is:
|
|
||||||
# "Zenroom_certifies_that_signature_is_correct!"
|
|
||||||
return True
|
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]:
|
def generate_a_keypair_reading_identity_from_data(self, my_name: str) -> Dict[str, Any]:
|
||||||
return self.generate_keypair(my_name)
|
return self.generate_keypair(my_name)
|
||||||
|
|
@ -445,7 +504,5 @@ class ZenroomServiceClient:
|
||||||
return self.symmetric_encrypt(header=header, message=message, shared_key=password)
|
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]:
|
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)
|
txt = self.symmetric_decrypt(secret_message=secret_message, shared_key=password)
|
||||||
return {"textDecrypted": txt}
|
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
|
import unittest
|
||||||
from unittest import mock
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
code_path = Path(__file__).parents[1] / "ca_core"
|
from ca_core.crypto.zenroom_service_client import ZenroomServiceClient
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class TestZenroomServiceClient(unittest.TestCase):
|
class TestZenroomServiceClient(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = ZenroomServiceClient()
|
||||||
|
|
||||||
@mock.patch("crypto.zenroom_service_client.urllib.request.urlopen")
|
def test_1_generate_keypair(self):
|
||||||
def test_generate_keypair_returns_keyring_and_private_key(self, m_urlopen):
|
result = self.client.generate_keypair("Alice")
|
||||||
payload = {
|
|
||||||
"User123456": {"keyring": {"ecdh": "PRIVKEY"}}
|
|
||||||
}
|
|
||||||
m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8"))
|
|
||||||
|
|
||||||
client = ZenroomServiceClient(base_url="http://localhost:3300")
|
self.assertEqual(result["my_name"], "Alice")
|
||||||
res = client.generate_keypair("User123456")
|
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")
|
def test_2_generate_public_key(self):
|
||||||
self.assertEqual(res["private_key"], "PRIVKEY")
|
keypair = self.client.generate_keypair("Alice")
|
||||||
self.assertEqual(res["keyring"], {"ecdh": "PRIVKEY"})
|
public_key = self.client.generate_public_key(keypair["keyring"])
|
||||||
self.assertNotIn("public_key", res)
|
|
||||||
|
|
||||||
req = m_urlopen.call_args[0][0]
|
self.assertIsInstance(public_key, str)
|
||||||
self.assertEqual(req.method, "POST")
|
self.assertTrue(public_key)
|
||||||
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"}})
|
|
||||||
|
|
||||||
@mock.patch("crypto.zenroom_service_client.urllib.request.urlopen")
|
def test_3_symmetric_encrypt(self):
|
||||||
def test_generate_public_key_returns_string(self, m_urlopen):
|
result = self.client.symmetric_encrypt(
|
||||||
payload = {"ecdh_public_key": "PUBKEY"}
|
header="test-header",
|
||||||
m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8"))
|
message="hello symmetric crypto",
|
||||||
|
shared_key="correct horse battery staple",
|
||||||
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"}},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@mock.patch("crypto.zenroom_service_client.urllib.request.urlopen")
|
self.assertIsInstance(result, dict)
|
||||||
def test_symmetric_decrypt_returns_plaintext(self, m_urlopen):
|
self.assertIn("checksum", result)
|
||||||
payload = {"textDecrypted": "PLAINTEXT"}
|
self.assertIn("header", result)
|
||||||
m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8"))
|
self.assertIn("iv", result)
|
||||||
|
self.assertIn("text", result)
|
||||||
|
|
||||||
client = ZenroomServiceClient(base_url="http://localhost:3300")
|
self.assertIsInstance(result["checksum"], str)
|
||||||
txt = client.symmetric_decrypt(secret_message={"iv": "x"}, shared_key="k")
|
self.assertIsInstance(result["header"], str)
|
||||||
self.assertEqual(txt, "PLAINTEXT")
|
self.assertIsInstance(result["iv"], str)
|
||||||
|
self.assertIsInstance(result["text"], str)
|
||||||
|
|
||||||
req = m_urlopen.call_args[0][0]
|
self.assertTrue(result["checksum"])
|
||||||
self.assertTrue(req.full_url.endswith("/api/Decrypt-the-message-with-the-password"))
|
self.assertTrue(result["header"])
|
||||||
sent = json.loads(req.data.decode("utf-8"))
|
self.assertTrue(result["iv"])
|
||||||
self.assertEqual(sent, {"data": {"secret_message": {"iv": "x"}, "password": "k"}})
|
self.assertTrue(result["text"])
|
||||||
|
|
||||||
@mock.patch("crypto.zenroom_service_client.urllib.request.urlopen")
|
def test_4_symmetric_decrypt(self):
|
||||||
def test_asymmetric_encrypt_returns_secret(self, m_urlopen):
|
plaintext = "hello symmetric crypto"
|
||||||
payload = {"secret": {"checksum": "C", "header": "H", "iv": "IV", "text": "T"}}
|
password = "correct horse battery staple"
|
||||||
m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8"))
|
|
||||||
|
|
||||||
client = ZenroomServiceClient(base_url="http://localhost:3300")
|
encrypted = self.client.symmetric_encrypt(
|
||||||
sec = client.asymmetric_encrypt(
|
header="test-header",
|
||||||
receiver_public_key="PUB",
|
message=plaintext,
|
||||||
sender_keyring={"ecdh": "PRIV"},
|
shared_key=password,
|
||||||
header="hdr",
|
|
||||||
message="msg",
|
|
||||||
)
|
|
||||||
self.assertEqual(sec, {"checksum": "C", "header": "H", "iv": "IV", "text": "T"})
|
|
||||||
|
|
||||||
req = m_urlopen.call_args[0][0]
|
|
||||||
self.assertTrue(req.full_url.endswith("/api/Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography"))
|
|
||||||
sent = json.loads(req.data.decode("utf-8"))
|
|
||||||
self.assertEqual(
|
|
||||||
sent,
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"reciever": {"public_key": "PUB"},
|
|
||||||
"sender": {"keyring": {"ecdh": "PRIV"}},
|
|
||||||
"header": "hdr",
|
|
||||||
"message": "msg",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@mock.patch("crypto.zenroom_service_client.urllib.request.urlopen")
|
decrypted = self.client.symmetric_decrypt(
|
||||||
def test_asymmetric_decrypt_returns_header_and_text(self, m_urlopen):
|
secret_message=encrypted,
|
||||||
payload = {"header": "HDR", "text": "TXT"}
|
shared_key=password,
|
||||||
m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8"))
|
|
||||||
|
|
||||||
client = ZenroomServiceClient(base_url="http://localhost:3300")
|
|
||||||
out = client.asymmetric_decrypt(
|
|
||||||
sender_public_key="PUB",
|
|
||||||
receiver_keyring={"ecdh": "PRIV"},
|
|
||||||
secret={"iv": "IV"},
|
|
||||||
)
|
|
||||||
self.assertEqual(out, {"header": "HDR", "text": "TXT"})
|
|
||||||
|
|
||||||
req = m_urlopen.call_args[0][0]
|
|
||||||
self.assertTrue(req.full_url.endswith("/api/Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography"))
|
|
||||||
sent = json.loads(req.data.decode("utf-8"))
|
|
||||||
self.assertEqual(
|
|
||||||
sent,
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"sender": {"public_key": "PUB"},
|
|
||||||
"reciever": {"keyring": {"ecdh": "PRIV"}},
|
|
||||||
"secret": {"iv": "IV"},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@mock.patch("crypto.zenroom_service_client.urllib.request.urlopen")
|
self.assertEqual(decrypted, plaintext)
|
||||||
def test_sign_objects_returns_response_and_validates_signatures(self, m_urlopen):
|
|
||||||
payload = {
|
|
||||||
"myMessage": "hello",
|
|
||||||
"myMessage.signature": {"r": "R", "s": "S"},
|
|
||||||
}
|
|
||||||
m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8"))
|
|
||||||
|
|
||||||
client = ZenroomServiceClient(base_url="http://localhost:3300")
|
def test_5_asymmetric_encrypt(self):
|
||||||
res = client.sign_objects(objects={"myMessage": "hello"}, signer_keyring={"ecdh": "PRIV"})
|
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")
|
result = self.client.asymmetric_encrypt(
|
||||||
self.assertEqual(res["myMessage.signature"]["r"], "R")
|
receiver_public_key=bob_public_key,
|
||||||
|
sender_keyring=alice["keyring"],
|
||||||
req = m_urlopen.call_args[0][0]
|
header="asym-header",
|
||||||
self.assertTrue(req.full_url.endswith("/api/Sign-objects-using-asymmetric-cryptography"))
|
message="hello bob",
|
||||||
sent = json.loads(req.data.decode("utf-8"))
|
|
||||||
self.assertEqual(
|
|
||||||
sent,
|
|
||||||
{"data": {"mySecretStuff": {"myMessage": "hello"}, "signer": {"keyring": {"ecdh": "PRIV"}}}},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@mock.patch("crypto.zenroom_service_client.urllib.request.urlopen")
|
self.assertIsInstance(result, dict)
|
||||||
def test_verify_signature_returns_true(self, m_urlopen):
|
self.assertIn("checksum", result)
|
||||||
payload = {
|
self.assertIn("header", result)
|
||||||
"myMessage": "hello",
|
self.assertIn("iv", result)
|
||||||
"output": ["Zenroom_certifies_that_signature_is_correct!"],
|
self.assertIn("text", result)
|
||||||
}
|
|
||||||
m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8"))
|
|
||||||
|
|
||||||
client = ZenroomServiceClient(base_url="http://localhost:3300")
|
self.assertIsInstance(result["checksum"], str)
|
||||||
ok = client.verify_signature(
|
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_field="myMessage",
|
||||||
message_value="hello",
|
message_value=signed["myMessage"],
|
||||||
signature={"r": "R", "s": "S"},
|
signature=signed["myMessage.signature"],
|
||||||
signer_public_key="PUB",
|
signer_public_key=alice_public_key,
|
||||||
)
|
|
||||||
self.assertTrue(ok)
|
|
||||||
|
|
||||||
req = m_urlopen.call_args[0][0]
|
|
||||||
self.assertTrue(req.full_url.endswith("/api/Verify-asymmetric-cryptography-signature"))
|
|
||||||
sent = json.loads(req.data.decode("utf-8"))
|
|
||||||
self.assertEqual(
|
|
||||||
sent,
|
|
||||||
{"data": {"myMessage": "hello", "myMessage.signature": {"r": "R", "s": "S"}, "signer": {"public_key": "PUB"}}},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@mock.patch("crypto.zenroom_service_client.urllib.request.urlopen")
|
self.assertTrue(verified)
|
||||||
def test_zenroom_error_is_raised(self, m_urlopen):
|
|
||||||
payload = {"exception": "boom", "zenroom_errors": {"logs": "fail"}}
|
|
||||||
m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8"))
|
|
||||||
|
|
||||||
client = ZenroomServiceClient(base_url="http://localhost:3300")
|
|
||||||
with self.assertRaises(ZenroomServiceError):
|
if __name__ == "__main__":
|
||||||
client.verify_signature(
|
unittest.main()
|
||||||
message_field="myMessage",
|
|
||||||
message_value="hello",
|
|
||||||
signature={"r": "R", "s": "S"},
|
|
||||||
signer_public_key="PUB",
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -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