Changeset d7499ca in flexoentity


Ignore:
Timestamp:
11/24/25 15:17:00 (7 weeks ago)
Author:
Enrico Schwass <ennoausberlin@…>
Branches:
master
Children:
2fd0536
Parents:
376e21b
Message:

Signature support for Linux and MacOS added

Files:
8 added
5 edited
1 moved

Legend:

Unmodified
Added
Removed
  • README.md

    r376e21b rd7499ca  
    1 #+TITLE: flexoentity
    2 #+SUBTITLE: A hardened entity base and deterministic identifier system for the Flex-O family
    3 #+AUTHOR: Enno
    4 #+DATE: 2025-10-20
    5 #+OPTIONS: toc:3 num:nil
    6 
    7 * Overview
    8 
    9 `flexoentity` provides the *identity and lifecycle backbone* for all Flex-O components 
     1
     2# Table of Contents
     3
     4-   [Overview](#org8360697)
     5-   [Flex-O ID Structure](#org89396ad)
     6-   [Lifecycle States](#orgd522c8c)
     7-   [Core Classes](#org381a3f6)
     8    -   [FlexOID](#org6abdc86)
     9    -   [`FlexoEntity`](#org15b9543)
     10-   [Integrity Verification](#org1617277)
     11-   [Real World Example](#orge08e5d3)
     12-   [Usage](#orgc8e7dd2)
     13-   [Serialization Example](#org1b3bbed)
     14-   [Entity Type and State Codes](#org5dfc109)
     15-   [Design Notes](#org659c733)
     16-   [Dependencies](#orga67f3a6)
     17-   [Integration](#org10ab165)
     18-   [License](#org48ba119)
     19
     20
     21
     22<a id="org8360697"></a>
     23
     24# Overview
     25
     26\`flexoentity\` provides the **identity and lifecycle backbone** for all Flex-O components 
    1027(Flex-O-Grader, Flex-O-Vault, Flex-O-Drill, …).
    1128
    12 It defines how entities such as questions, media, catalogs, and exams are *identified, versioned, signed, and verified* — all without any external database dependencies.
     29It defines how entities such as questions, media, catalogs, and exams are **identified, versioned, signed, and verified** — all without any external database dependencies.
    1330
    1431At its heart lie two modules:
    1532
    16 - =id_factory.py= – robust, cryptographically-verifiable *Flex-O ID generator*
    17 - =versioned_entity.py= – abstract *base class for all versioned entities*
     33-   `id_factory.py` – robust, cryptographically-verifiable **Flex-O ID generator**
     34-   `flexo_entity.py` – abstract **base class for all versioned entities**
    1835
    1936Together, they form a compact yet powerful foundation for audit-ready, reproducible data structures across offline and air-gapped deployments.
    2037
    21 ✳️ Design Goals
    22 |----------------|--------------------------------------------------------------------------------------------------------|
    23 | Goal           | Description                                                                                            |
    24 |----------------|--------------------------------------------------------------------------------------------------------|
    25 | *Determinism*  | IDs are derived from canonicalized entity content — identical input always yields identical ID prefix. |
    26 | *Integrity*    | BLAKE2s hashing and digital signatures protect against manual tampering.                               |
    27 | *Traceability* | Version numbers (=@001A=, =@002S=, …) track entity lifecycle transitions.                              |
    28 | *Stability*    | Hash prefixes remain constant across state changes; only version and state suffixes evolve.            |
    29 | *Auditability* | Every entity can be serialized, verified, and reconstructed without hidden dependencies.               |
    30 | *Simplicity*   | Pure-Python, zero external libraries, self-contained and easy to embed.                                |
    31 |----------------|--------------------------------------------------------------------------------------------------------|
    32 
    33 * Flex-O ID Structure
    34 
    35 Each entity carries a unique *Flex-O ID*, generated by =FlexOID.generate()=.
    36 
    37 #+BEGIN_EXAMPLE
    38 AF-Q250101-9A4C2D@003S
    39 #+END_EXAMPLE
    40 
    41 |-----------|----------|---------------------------------------------|
    42 | Segment   | Example  | Meaning                                     |
    43 |-----------|----------|---------------------------------------------|
    44 | *Domain*  | =AF=     | Logical scope (e.g. "Air Force")            |
    45 | *Type*    | =Q=      | Entity type (e.g. Question)                 |
    46 | *Date*    | =250101= | UTC creation date (YYMMDD)                  |
    47 | *Hash*    | =9A4C2D= | 6-digit BLAKE2s digest of canonical content |
    48 | *Version* | =003=    | Sequential version counter                  |
    49 | *State*   | =S=      | Lifecycle state (=D=, =A=, =S=, =P=, =O=)   |
    50 |-----------|----------|---------------------------------------------|
    51 
    52 Hash collisions within a single session are automatically disambiguated (=-1=, =-2=, …).
    53 
    54 * Lifecycle States
    55 
    56 |-----------------------|---------|-----------------------------|
    57 | State                 | Abbrev. | Description                 |
    58 |-----------------------|---------|-----------------------------|
    59 | *DRAFT*               | =D=     | Editable, not yet validated |
    60 | *APPROVED*            | =A=     | Reviewed and accepted       |
    61 | *APPROVED_AND_SIGNED* | =S=     | Cryptographically signed    |
    62 | *PUBLISHED*           | =P=     | Released to consumers       |
    63 | *OBSOLETE*            | =O=     | Archived or replaced        |
    64 |-----------------------|---------|-----------------------------|
     38-   Design Goals
     39
     40<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">
     41
     42
     43<colgroup>
     44<col  class="org-left" />
     45
     46<col  class="org-left" />
     47</colgroup>
     48<thead>
     49<tr>
     50<th scope="col" class="org-left">Goal</th>
     51<th scope="col" class="org-left">Description</th>
     52</tr>
     53</thead>
     54<tbody>
     55<tr>
     56<td class="org-left"><b>Determinism</b></td>
     57<td class="org-left">IDs are derived from canonicalized entity content — identical input always yields identical ID prefix.</td>
     58</tr>
     59
     60<tr>
     61<td class="org-left"><b>Integrity</b></td>
     62<td class="org-left">BLAKE2s hashing and digital signatures protect against manual tampering.</td>
     63</tr>
     64
     65<tr>
     66<td class="org-left"><b>Traceability</b></td>
     67<td class="org-left">Version numbers (<code>@001A</code>, <code>@002S</code>, …) track entity lifecycle transitions.</td>
     68</tr>
     69
     70<tr>
     71<td class="org-left"><b>Stability</b></td>
     72<td class="org-left">Hash prefixes remain constant across state changes; only version and state suffixes evolve.</td>
     73</tr>
     74
     75<tr>
     76<td class="org-left"><b>Auditability</b></td>
     77<td class="org-left">Every entity can be serialized, verified, and reconstructed without hidden dependencies.</td>
     78</tr>
     79
     80<tr>
     81<td class="org-left"><b>Simplicity</b></td>
     82<td class="org-left">Pure-Python, zero external libraries, self-contained and easy to embed.</td>
     83</tr>
     84</tbody>
     85</table>
     86
     87
     88<a id="org89396ad"></a>
     89
     90# Flex-O ID Structure
     91
     92Each entity carries a unique **Flex-O ID**, generated by `FlexOID.generate()`.
     93
     94    AF-I250101-9A4C2D@003S
     95
     96<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">
     97
     98
     99<colgroup>
     100<col  class="org-left" />
     101
     102<col  class="org-left" />
     103
     104<col  class="org-left" />
     105</colgroup>
     106<thead>
     107<tr>
     108<th scope="col" class="org-left">Segment</th>
     109<th scope="col" class="org-left">Example</th>
     110<th scope="col" class="org-left">Meaning</th>
     111</tr>
     112</thead>
     113<tbody>
     114<tr>
     115<td class="org-left"><b>Domain</b></td>
     116<td class="org-left"><code>AF or PY_LANG</code></td>
     117<td class="org-left">Uppercase - Logical scope (e.g. &ldquo;Air Force&rdquo;)</td>
     118</tr>
     119
     120<tr>
     121<td class="org-left"><b>Type</b></td>
     122<td class="org-left"><code>I</code></td>
     123<td class="org-left">Entity type (e.g. ITEM)</td>
     124</tr>
     125
     126<tr>
     127<td class="org-left"><b>Date</b></td>
     128<td class="org-left"><code>250101</code></td>
     129<td class="org-left">UTC creation date (YYMMDD)</td>
     130</tr>
     131
     132<tr>
     133<td class="org-left"><b>Hash</b></td>
     134<td class="org-left"><code>9A4C2D4F6E53</code></td>
     135<td class="org-left">12-digit BLAKE2s digest of canonical content</td>
     136</tr>
     137
     138<tr>
     139<td class="org-left"><b>Version</b></td>
     140<td class="org-left"><code>003</code></td>
     141<td class="org-left">Sequential version counter</td>
     142</tr>
     143
     144<tr>
     145<td class="org-left"><b>State</b></td>
     146<td class="org-left"><code>S</code></td>
     147<td class="org-left">Lifecycle state (D, A, S, P, O)</td>
     148</tr>
     149</tbody>
     150</table>
     151
     152
     153<a id="orgd522c8c"></a>
     154
     155# Lifecycle States
     156
     157<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">
     158
     159
     160<colgroup>
     161<col  class="org-left" />
     162
     163<col  class="org-left" />
     164
     165<col  class="org-left" />
     166</colgroup>
     167<thead>
     168<tr>
     169<th scope="col" class="org-left">State</th>
     170<th scope="col" class="org-left">Abbrev.</th>
     171<th scope="col" class="org-left">Description</th>
     172</tr>
     173</thead>
     174<tbody>
     175<tr>
     176<td class="org-left"><b>DRAFT</b></td>
     177<td class="org-left"><code>D</code></td>
     178<td class="org-left">Editable, not yet validated</td>
     179</tr>
     180
     181<tr>
     182<td class="org-left"><b>APPROVED</b></td>
     183<td class="org-left"><code>A</code></td>
     184<td class="org-left">Reviewed and accepted</td>
     185</tr>
     186
     187<tr>
     188<td class="org-left"><b>APPROVED<sub>AND</sub><sub>SIGNED</sub></b></td>
     189<td class="org-left"><code>S</code></td>
     190<td class="org-left">Cryptographically signed</td>
     191</tr>
     192
     193<tr>
     194<td class="org-left"><b>PUBLISHED</b></td>
     195<td class="org-left"><code>P</code></td>
     196<td class="org-left">Released to consumers</td>
     197</tr>
     198
     199<tr>
     200<td class="org-left"><b>OBSOLETE</b></td>
     201<td class="org-left"><code>O</code></td>
     202<td class="org-left">Archived or replaced</td>
     203</tr>
     204</tbody>
     205</table>
    65206
    66207Transitions follow a strict progression:
    67 #+BEGIN_EXAMPLE
    68 DRAFT → APPROVED → APPROVED_AND_SIGNED → PUBLISHED → OBSOLETE
    69 #+END_EXAMPLE
    70 
    71 Version increments occur automatically for all *stable* transitions (APPROVED, SIGNED, PUBLISHED).
    72 
    73 * Core Classes
    74 
    75 ** =FlexOID=
     208
     209    DRAFT -> APPROVED -> APPROVED_AND_SIGNED -> PUBLISHED -> OBSOLETE
     210
     211Only DRAFT entities can be deleted - all others got OBSOLETE mark instead
     212
     213
     214<a id="org381a3f6"></a>
     215
     216# Core Classes
     217
     218
     219<a id="org6abdc86"></a>
     220
     221## FlexOID
    76222
    77223A lightweight immutable class representing the full identity of an entity.
    78224
    79 *Highlights*
    80 - =generate(domain, entity_type, estate, text, version=1)= → create a new ID
    81 - =next_version(oid)= → increment version safely
    82 - =clone_new_base(domain, entity_type, estate, text)= → start a new lineage
    83 - Deterministic prefix, state-dependent signature
    84 
    85 ** =FlexoEntity=
     225**Highlights**
     226
     227-   safe<sub>generate</sub>(domain, entity<sub>type</sub>, estate, text, version=1, repo) -> create a new ID
     228-   next<sub>version</sub>(oid) -> increment version safely
     229-   clone<sub>new</sub><sub>base</sub>(domain, entity<sub>type</sub>, estate, text) -> start a new lineage
     230-   Deterministic prefix, state-dependent signature
     231
     232
     233<a id="org15b9543"></a>
     234
     235## `FlexoEntity`
     236
    86237Abstract base class for all versioned entities (e.g., Question, Exam, Catalog).
    87238
    88239Implements:
    89 - ID lifecycle management (=approve()=, =sign()=, =publish()=, =obsolete()=)
    90 - JSON serialization (=to_json()=, =from_json()=)
    91 - Integrity verification (=verify_integrity(entity)=)
    92 - Controlled state transitions with automatic timestamps
     240
     241-   ID lifecycle management (approve(), sign(), publish(), obsolete())
     242-   Serialization (to<sub>json</sub>(), from<sub>json</sub>(), to<sub>dict</sub>(), from<sub>dict</sub>())
     243-   Integrity verification (verify<sub>integrity</sub>(entity))
     244-   Controlled state transitions with automatic timestamps
    93245
    94246Subclasses define a single property:
    95247
    96 #+BEGIN_SRC python
    97 @property
    98 def text_seed(self) -> str:
    99     """Canonical text or core content for hashing."""
    100 #+END_SRC
    101 
    102 * Integrity Verification
     248    @property
     249    def text_seed(self) -> str:
     250        """Canonical text or core content for hashing."""
     251
     252
     253<a id="org1617277"></a>
     254
     255# Integrity Verification
    103256
    104257Each entity can self-verify its integrity:
    105258
    106 #+BEGIN_SRC python
    107 entity = Question("AF", EntityType.ITEM, "What is Ohm’s law?")
    108 json_str = entity.to_json()
    109 reloaded = Question.from_json(json_str)
    110 
    111 assert FlexoEntity.verify_integrity(reloaded)
    112 #+END_SRC
    113 
    114 If the file is tampered with (e.g. "Ohm’s" → "Omm’s"), verification fails:
    115 
    116 #+BEGIN_SRC python
    117 assert not FlexoEntity.verify_integrity(reloaded)
    118 #+END_SRC
    119 
    120 * Example
    121 #+BEGIN_SRC python
    122 from flexoentity.versioned_entity import FlexoEntity, EntityType, EntityState
    123 
    124 class Question(FlexoEntity):
    125     def __init__(self, domain, text):
    126         self._text = text
    127         super().__init__(domain, EntityType.ITEM)
    128 
    129     @property
    130     def text_seed(self):
    131         return self._text
    132 
    133 * Usage
    134 #+BEGIN_SRC python
    135 q = Question("AF", "What is Ohm’s law?")
    136 print(q.flexo_id)             # AF-Q250101-9A4C2D@001D
    137 q.approve()
    138 print(q.flexo_id)             # AF-Q250101-9A4C2D@002A
    139 q.sign()
    140 print(q.flexo_id)             # AF-Q250101-9A4C2D@003S
    141 #+END_SRC
    142 
    143 * JSON Example
    144 #+BEGIN_SRC json
     259    entity = Question.with_domain_id(domain_id="AF", text="What is Ohm’s law?", topic="Electronics")
     260    json_str = entity.to_json()
     261    reloaded = Question.from_json(json_str)
     262   
     263    assert FlexoEntity.verify_integrity(reloaded)
     264
     265If the file is tampered with (e.g. &ldquo;Ohm’s&rdquo; → &ldquo;Omm’s&rdquo;), verification fails:
     266
     267
     268<a id="orge08e5d3"></a>
     269
     270# Real World Example
     271
     272Below you can see the implementation of a dedicated FlexoEntity class, used for Domains.
     273We set an ENTITY<sub>TYPE</sub> and define the needed fields in the data class. We define how to create
     274a default object, the text<sub>seed</sub> (it is easy because the domain id is unique and therefore sufficient)
     275and the methods for serialization.
     276
     277    from uuid import UUID
     278    from dataclasses import dataclass
     279    from flexoentity import FlexOID, FlexoEntity, EntityType
     280   
     281    @dataclass
     282    class Domain(FlexoEntity):
     283        """
     284        I am a helper class to provide more information than just a
     285        domain abbreviation in FlexOID, doing mapping and management
     286        """
     287   
     288        ENTITY_TYPE = EntityType.DOMAIN
     289   
     290        fullname: str = ""
     291        description: str = ""
     292        classification: str = "UNCLASSIFIED"
     293   
     294        @classmethod
     295        def default(cls):
     296            """Return the default domain object."""
     297            return cls.with_domain_id(domain_id="GEN_GENERIC",
     298                                      fullname="Generic Domain", classification="UNCLASSIFIED")
     299   
     300        @property
     301        def text_seed(self) -> str:
     302            return self.domain_id
     303   
     304        def to_dict(self):
     305            base = super().to_dict()
     306            base.update({
     307                "flexo_id": self.flexo_id,
     308                "domain_id": self.domain_id,
     309                "fullname": self.fullname,
     310                "description": self.description,
     311                "classification": self.classification,
     312            })
     313            return base
     314   
     315        @classmethod
     316        def from_dict(cls, data):
     317            # Must have flexo_id
     318            if "flexo_id" not in data:
     319                raise ValueError("Domain serialization missing 'flexo_id'.")
     320   
     321            flexo_id = FlexOID(data["flexo_id"])
     322   
     323            obj = cls(
     324                fullname=data.get("fullname", ""),
     325                description=data.get("description", ""),
     326                classification=data.get("classification", "UNCLASSIFIED"),
     327                flexo_id=flexo_id,
     328                _in_factory=True
     329            )
     330   
     331            # Restore metadata
     332            obj.origin = data.get("origin")
     333            obj.fingerprint = data.get("fingerprint", "")
     334            obj.originator_id = (
     335                UUID(data["originator_id"]) if data.get("originator_id") else None
     336            )
     337            obj.owner_id = (
     338                UUID(data["owner_id"]) if data.get("owner_id") else None
     339            )
     340   
     341            return obj
     342
     343
     344<a id="orgc8e7dd2"></a>
     345
     346# Usage
     347
     348    d = Domain.default()
     349    print(d.flexo_id)             # GEN_GENERIC-D251124-67C2CAE292CE@001D
     350    d.approve()
     351    print(d.flexo_id)             # GEN_GENERIC-D251124-67C2CAE292CE@001A
     352    d.sign()
     353    print(d.flexo_id)             # GEN_GENERIC-D251124-67C2CAE292CE@001S
     354
     355
     356<a id="org1b3bbed"></a>
     357
     358# Serialization Example
     359
     360    {
     361        'flexo_id': FlexOID(GEN_GENERIC-D251124-29CE0F4BE59D@001S),
     362        'fingerprint': '534BD2EC5C5511F1',
     363        'origin': FlexOID(GEN_GENERIC-D251124-67C2CAE292CE@001D),
     364        'originator_id': '00000000-0000-0000-0000-000000000000',
     365        'owner_id': '00000000-0000-0000-0000-000000000000',
     366        'domain_id': 'GEN_GENERIC',
     367        'fullname': 'Generic Domain',
     368        'description': '',
     369        'classification': 'UNCLASSIFIED'}
     370
     371\#+BEGIN<sub>SRC</sub> js
     372
    145373{
    146   "domain": "AF",
    147   "entity_type": "QUESTION",
    148   "text_seed": "What is Ohm’s law?",
    149   "state": "APPROVED_AND_SIGNED",
    150   "version": 3,
    151   "flexo_id": "AF-Q250101-9A4C2D@003S",
    152   "signature": "1E3A9F2A8B7C4D11",
     374        &ldquo;flexo<sub>id</sub>&rdquo;: &ldquo;GEN<sub>GENERIC</sub>-D251124-29CE0F4BE59D@001S&rdquo;,
     375        &ldquo;fingerprint&rdquo;: &ldquo;534BD2EC5C5511F1&rdquo;,
     376        &ldquo;origin&rdquo;: &ldquo;GEN<sub>GENERIC</sub>-D251124-67C2CAE292CE@001D&rdquo;,
     377        &ldquo;originator<sub>id</sub>&rdquo;: &ldquo;00000000-0000-0000-0000-000000000000&rdquo;,
     378        &ldquo;owner<sub>id</sub>&rdquo;: &ldquo;00000000-0000-0000-0000-000000000000&rdquo;,
     379        &ldquo;domain<sub>id</sub>&rdquo;: &ldquo;GEN<sub>GENERIC</sub>&rdquo;,
     380        &ldquo;fullname&rdquo;: &ldquo;Generic Domain&rdquo;,
     381        &ldquo;description&rdquo;: &ldquo;&rdquo;,
     382        &ldquo;classification&rdquo;: &ldquo;UNCLASSIFIED&rdquo;
    153383}
    154 #+END_SRC
    155 
    156 * Entity Type and State Codes
    157 
    158 |-------------|------|------------------------------|
    159 | EntityType  | Code | Typical Use                  |
    160 |-------------|------|------------------------------|
    161 | QUESTION    | Q    | Exam question                |
    162 | CATALOG     | CAT  | Question or media collection |
    163 | EXAM        | EX   | Composed test instance       |
    164 | DATABASE    | DB   | Persistent store             |
    165 | CERTIFICATE | CERT | Digital or approval document |
    166 |-------------|------|------------------------------|
    167 
    168 |---------------------|------|-------------------|
    169 | EntityState         | Code | Meaning           |
    170 |---------------------|------|-------------------|
    171 | DRAFT               | D    | Work in progress  |
    172 | APPROVED            | A    | Reviewed          |
    173 | APPROVED_AND_SIGNED | S    | Signed version    |
    174 | PUBLISHED           | P    | Publicly released |
    175 | OBSOLETE            | O    | Deprecated        |
    176 |---------------------|------|-------------------|
    177 
    178 * Design Notes
    179 - *Hash Stability:* Only domain, entity type, and content text influence the hash.
    180   This ensures consistent prefixes across state changes.
    181 - *State-Dependent Signatures:* Each lifecycle stage has its own signature seed.
    182   Modifying a file without re-signing invalidates integrity.
    183 - *Obsolescence Threshold:* Version numbers above 900 trigger warnings;
    184   beyond 999 are considered obsolete.
    185 - *Clone Lineages:* Cloning an entity resets versioning but preserves metadata lineage.
    186 
    187 * Dependencies
    188 - Python 3.11+
    189 - Standard library only (=hashlib=, =json=, =datetime=, =enum=, =dataclasses=)
    190 
    191 No external packages. Fully compatible with *Guix*, *air-gapped* deployments, and *reproducible builds*.
    192 
    193 * Integration
    194 `flexoentity` is imported by higher-level modules such as:
    195 
    196 - *Flex-O-Grader* → manages question catalogs and exam bundles
    197 - *Flex-O-Vault* → provides persistent media storage with metadata integrity
    198 - *Flex-O-Drill* → uses versioned entities for training simulations
     384
     385
     386<a id="org5dfc109"></a>
     387
     388# Entity Type and State Codes
     389
     390<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">
     391
     392
     393<colgroup>
     394<col  class="org-left" />
     395
     396<col  class="org-left" />
     397
     398<col  class="org-left" />
     399</colgroup>
     400<thead>
     401<tr>
     402<th scope="col" class="org-left">EntityType</th>
     403<th scope="col" class="org-left">Code</th>
     404<th scope="col" class="org-left">Typical Use</th>
     405</tr>
     406</thead>
     407<tbody>
     408<tr>
     409<td class="org-left">GENERIC</td>
     410<td class="org-left">G</td>
     411<td class="org-left">Generic entities that does not fit other types yet or are temporarily only</td>
     412</tr>
     413
     414<tr>
     415<td class="org-left">DOMAIN</td>
     416<td class="org-left">D</td>
     417<td class="org-left">Every Domain is of this type</td>
     418</tr>
     419
     420<tr>
     421<td class="org-left">MEDIA</td>
     422<td class="org-left">M</td>
     423<td class="org-left">Every media item belongs to this type, e.g. Pictures, Audio, Video</td>
     424</tr>
     425
     426<tr>
     427<td class="org-left">ITEM</td>
     428<td class="org-left">I</td>
     429<td class="org-left">An Entity what is usually used in a collection, e.g. Questions in a test</td>
     430</tr>
     431
     432<tr>
     433<td class="org-left">COLLECTION</td>
     434<td class="org-left">C</td>
     435<td class="org-left">A collection of items, as an Exam or a catalog</td>
     436</tr>
     437
     438<tr>
     439<td class="org-left">TEXT</td>
     440<td class="org-left">T</td>
     441<td class="org-left">A text document</td>
     442</tr>
     443
     444<tr>
     445<td class="org-left">HANDOUT</td>
     446<td class="org-left">H</td>
     447<td class="org-left">A published document</td>
     448</tr>
     449
     450<tr>
     451<td class="org-left">OUTPUT</td>
     452<td class="org-left">O</td>
     453<td class="org-left">The output of a computation</td>
     454</tr>
     455
     456<tr>
     457<td class="org-left">RECORD</td>
     458<td class="org-left">R</td>
     459<td class="org-left">Record type data, as bibliography entries</td>
     460</tr>
     461
     462<tr>
     463<td class="org-left">SESSION</td>
     464<td class="org-left">S</td>
     465<td class="org-left">A unique session, e.g. managed by a session manager</td>
     466</tr>
     467
     468<tr>
     469<td class="org-left">USER</td>
     470<td class="org-left">U</td>
     471<td class="org-left">User objects</td>
     472</tr>
     473
     474<tr>
     475<td class="org-left">CONFIG</td>
     476<td class="org-left">F</td>
     477<td class="org-left">CONFIG files that need to be tracked over time and state</td>
     478</tr>
     479
     480<tr>
     481<td class="org-left">EVENT</td>
     482<td class="org-left">E</td>
     483<td class="org-left">Events that have to be tracked over time, as status messages or orders</td>
     484</tr>
     485
     486<tr>
     487<td class="org-left">ATTESTATION</td>
     488<td class="org-left">X</td>
     489<td class="org-left">Entities that attest a formal technical (not human) check e.g. Signatures</td>
     490</tr>
     491</tbody>
     492</table>
     493
     494<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">
     495
     496
     497<colgroup>
     498<col  class="org-left" />
     499
     500<col  class="org-left" />
     501
     502<col  class="org-left" />
     503</colgroup>
     504<thead>
     505<tr>
     506<th scope="col" class="org-left">EntityState</th>
     507<th scope="col" class="org-left">Code</th>
     508<th scope="col" class="org-left">Meaning</th>
     509</tr>
     510</thead>
     511<tbody>
     512<tr>
     513<td class="org-left">DRAFT</td>
     514<td class="org-left">D</td>
     515<td class="org-left">Work in progress</td>
     516</tr>
     517
     518<tr>
     519<td class="org-left">APPROVED</td>
     520<td class="org-left">A</td>
     521<td class="org-left">Reviewed</td>
     522</tr>
     523
     524<tr>
     525<td class="org-left">APPROVED<sub>AND</sub><sub>SIGNED</sub></td>
     526<td class="org-left">S</td>
     527<td class="org-left">Signed version</td>
     528</tr>
     529
     530<tr>
     531<td class="org-left">PUBLISHED</td>
     532<td class="org-left">P</td>
     533<td class="org-left">Publicly released</td>
     534</tr>
     535
     536<tr>
     537<td class="org-left">OBSOLETE</td>
     538<td class="org-left">O</td>
     539<td class="org-left">Deprecated</td>
     540</tr>
     541</tbody>
     542</table>
     543
     544
     545<a id="org659c733"></a>
     546
     547# Design Notes
     548
     549-   **Hash Stability:** Only domain, entity type, and content text influence the hash.
     550    This ensures consistent prefixes across state changes.
     551-   **State-Dependent Signatures:** Each lifecycle stage has its own signature seed.
     552    Modifying a file without re-signing invalidates integrity.
     553-   **Obsolescence Threshold:** Version numbers above 900 trigger warnings;
     554    beyond 999 are considered obsolete.
     555-   **Clone Lineages:** Cloning an entity resets versioning but preserves metadata lineage.
     556
     557
     558<a id="orga67f3a6"></a>
     559
     560# Dependencies
     561
     562-   Python 3.11+
     563-   Standard library only (`hashlib`, `json`, `datetime`, `enum`, `dataclasses`)
     564
     565No external packages. Fully compatible with **Guix**, **air-gapped** deployments, and **reproducible builds**.
     566
     567
     568<a id="org10ab165"></a>
     569
     570# Integration
     571
     572\`flexoentity\` is imported by higher-level modules such as:
     573
     574-   **Flex-O-Grader** → manages question catalogs and exam bundles
     575-   **Flex-O-Vault** → provides persistent media storage with metadata integrity
     576-   **Flex-O-Drill** → uses versioned entities for training simulations
    199577
    200578All share the same identity and versioning logic — ensuring that
    201 *what was approved, signed, and published remains provably authentic.*
    202 
    203 * License
     579**what was approved, signed, and published remains provably authentic.**
     580
     581
     582<a id="org48ba119"></a>
     583
     584# License
     585
    204586MIT License 2025
    205 Part of the *Flex-O family* by Flex-O-Dyne GmbH
     587Part of the **Flex-O family** by Flex-O-Dyne GmbH
    206588Designed for reproducible, audit-ready, human-centered software.
     589
  • flexoentity/__init__.py

    r376e21b rd7499ca  
    1515from .flexo_collection import FlexoCollection
    1616from .domain import Domain
     17from .flexo_signature import FlexoSignature, CertificateReference
     18from .signing_backends import get_signing_backend
    1719
    1820__all__ = [
     
    2426    "EntityState",
    2527    "FlexoCollection",
     28    "FlexoSignature",
     29    "CertificateReference",
     30    "get_signing_backend",
    2631    "logger"
    2732]
  • flexoentity/domain.py

    r376e21b rd7499ca  
    11from uuid import UUID
    22from dataclasses import dataclass
    3 from flexoentity import FlexOID, FlexoEntity, EntityType, EntityState
     3from flexoentity import FlexOID, FlexoEntity, EntityType
    44
    55@dataclass
  • flexoentity/flexo_entity.py

    r376e21b rd7499ca  
    8787    CONFIG = "F"
    8888    EVENT = "E"
     89    ATTESTATION = "X"
    8990
    9091    @classmethod
  • org/FlexoEntityTalk.org

    r376e21b rd7499ca  
    7171enthält, um möglichst eindeutig, aber nicht zu lang zu sein.
    7272
     73AF-I251022-70F759@001D
     74│  │ │       │     │ │
     75│  │ │       │     │ └── State (Draft, Approved, Signed, Published, Obsolete)
     76│  │ │       │     └──── Version
     77│  │ │       └────────── Hash (6 bytes, 12 Stellen)
     78│  │ └────────────────── Date (YYMMDD)
     79│  └──────────────────── Entity type (ITEM)
     80└─────────────────────── Domain (e.g. Air force)
     81
     82In der ersten Variante der FlexOID habe ich mich mit einem 6-stelligen Hash zufrieden gegeben,
     83weil das einfach lesbarer ist. Warum reicht das nicht?
     84
     85** Das Geburtstags-Paradoxon
     86
     87Der obige 3-Byte Hash liefert mir etwas über 16 Millionen unterschiedlich Varianten.
     88Das klingt viel. Ist es aber nicht. Wieviele Schüler müssen nacheinander die Klasse
     89betreten bis sich zwei finden, die mit mehr als 50 Prozent Wahrscheinlichkeit am gleichen
     90Tag Geburtstag (also ein identisches Merkmal) haben?
     91
     92** Lösung
     93
     94Ab 23 Schülern beträgt die Wahrscheinlichkeit 50 % (näherungsweise Wurzel 365 Tagen)
     95
     96Bei 47 Schülern beträgt die Wahrscheinlichkeit bereits 95 Prozent
     97
     98Unsere 3 Bytes ergeben zwar 16.000.000 mögliche Varianten - im Gegenzug zu den 365 Tagen im Jahr
     99aus dem Geburtstagsbeispiel - aber da die Wahrscheinlichkeit zur Kollision hier bei
     100etwa Wurzel 16 Mio. liegt, bekommt man bereits bei 4000 Neuzugängen die ersten
     101Übereinstimmungen im Hash.
     102
     103Der Hash ist also nicht gut genug, weil man sehr schnell und sehr häufig, diese
     104Übereinstimmungen feststellen und behandeln müsste, wenn man weiterhin eindeutige
     105Zuordnungen treffen will.
     106
     107Wenn man die Ausgabe der Hashfunktion auf 6 Bytes erweitert, kommen die ersten Kollisionen
     108erst bei etwa 20 Mio erzeugten Hashes (Fingerabdrücken) und die kann man dann ohne
     109Einbußen gesondert behandeln (Salzen), weil es so selten passiert.
     110
     111Übrigens, wenn man beim Menschen den Daumenabdruck nimmt und sich dabei auf 12 Merkmale
     112beschränkt und sehr lockere Toleranzen ansetzt (was man in der Praxis nicht macht),
     113hat man bereits nach 14000 Menschen eine Übereinstimmung. Bei 24 Merkmalen und sehr
     114lockeren Toleranzen, hat man bei etwa nach 9 Mio. Menschen eine ungefähre Übereinstimmung.
     115Da muss die Polizei schon sehr schlampig arbeiten, damit man fälschlicherweise beschuldigt wird.
     116Die Zahlen in der Realität sind sogar noch deutlich höher.
     117
     118** FlexoEntity
     119
     120Nun haben wir gesehen, dass wir mit der FlexOID (mit 6-Byte Hash) sehr viele unterschiedliche
     121Dinge eindeutig bestimmen können. Da unsere FlexOID erstmal nur eine Zeichenfolge ist,
     122brauchen wir etwas das damit umgehen kann und was dafür verantwortlich ist. Das ist die FlexoEntity.
     123
     124Sie beinhaltet zusätzlich ein Origin-Feld, wo festgehalten wird, woher diese
     125Entität stammt (beispielsweise aus einer Hashkollision oder einer Änderung an den weiteren Daten)
     126
     127Jede Klasse, die von FlexoEntity erbt, muss zwingend die Methode "text_seed" implementieren,
     128mit der der Algorithmus einer Hash-Funktion gefüttert wird, aus der dann der 6-Byte Hash herauspurzelt.
     129Hashfunktionen sind z.B. MD5, SHA1, SHA256 oder wie von mir genutzt: Blake2s.
     130Die Mathematik dahinter ist recht aufwändig, aber wer sich mal einlesen möchte
     131
     132- Hashing in Smalltalk: Theory and Practice von Andres Valloud
     133
     134Damit die Hash-Funktion genug Eingabedaten pro Entity hat, muss man sich überlegen, welche Merkmale
     135einer Entität man durch "text_seed" übermittelt.
     136
     137** text_seed
     138
     139Man kann beliebige Klassen von FlexoEntity ableiten und erhält ohne Aufwand die Funktionalität
     140zur eindeutigen Identifizierung und zur Lebenszyklus-Verwaltung
     141
     142Das ist das Beispiel einer ChoiceQuestion, also einer Testfrage, wo mögliche Antworten enthalten sind
     143
     144    @property
     145    def text_seed(self) -> str:
     146        """Include answer options (and points) for deterministic ID generation."""
     147        base = super().text_seed
     148        if not self.options:
     149            return base
     150
     151        joined = "|".join(
     152            f"{opt.text.strip()}:{opt.points}"
     153            for opt in sorted(self.options, key=lambda o: o.text.strip().lower())
     154        )
     155        return f"{base}::{joined}"
     156
     157** Lebenszyklus
     158
     159Der Lebenszyklus einer Entität folgt dieser Reihenfolge und ist nicht umkehrbar
     160
     161- Entwurf (DRAFT)
     162- Genehmigt (APPROVED)
     163- Unterschrieben (APPROVED_AND_SIGNED)
     164- Veröffentlicht (PUBLISHED)
     165- Veraltet (OBSOLETE)
     166
     167Eine Entität, die bereits die Stufe Veröffentlicht erreicht hat, kann nicht in die Stufe
     168(nur) Unterschrieben zurückkehren. Daher ist auch die Lebenszyklusstufe in der ID kodiert
     169(letztes Symbol der FlexOID)
     170
     171Beispiele für Entitäten:
     172
     173- Testfrage
     174- Fragenkatalog
     175- Einstufungstest
     176- Zertifikat
     177
     178Ein veröffentlichter Einstufungstest kann nur Fragen beinhalten, die ihrerseits die Stufe Veröffentlicht
     179erreicht haben. Ein Zertifikat kann nur ausgestellt werden, wenn der passende Einstufungstest die Stufe
     180Veröffentlicht hat. Das origin-Feld des Zertifikats sollte sinnvollerweise die ID des Tests enthalten.
     181
     182** Erhöhung der Versionsnummer oder neue ID
     183
     184Sobald - aus Gründen - eine neue ID vergeben werden muss, wird ggf. die Ursprungs-ID
     185im Feld origin der neuen FlexoEntity gespeichert. So ist immer ein Suchen im Stammbaum möglich.
     186
    73187AF-Q251022-70F759@001D
    74188│  │ │       │     │ │
     
    80194└─────────────────────── Domain (e.g. Air force)
    81195
    82 In der ersten Variante der FlexOID habe ich mich mit einem 6-stelligen Hash zufrieden gegeben,
    83 weil das einfach lesbarer ist. Warum reicht das nicht?
    84 
    85 ** Das Geburtstags-Paradoxon
    86 
    87 Der obige 3-Byte Hash liefert mir etwas über 16 Millionen unterschiedlich Varianten.
    88 Das klingt viel. Ist es aber nicht. Wieviele Schüler müssen nacheinander die Klasse
    89 betreten bis sich zwei finden, die mit mehr als 50 Prozent Wahrscheinlichkeit am gleichen
    90 Tag Geburtstag (also ein identisches Merkmal) haben?
    91 
    92 ** Lösung
    93 
    94 Ab 23 Schülern beträgt die Wahrscheinlichkeit 50 % (näherungsweise Wurzel 365 Tagen)
    95 
    96 Bei 47 Schülern beträgt die Wahrscheinlichkeit bereits 95 Prozent
    97 
    98 Unsere 3 Bytes ergeben zwar 16.000.000 mögliche Varianten - im Gegenzug zu den 365 Tagen im Jahr
    99 aus dem Geburtstagsbeispiel - aber da die Wahrscheinlichkeit zur Kollision hier bei
    100 etwa Wurzel 16 Mio. liegt, bekommt man bereits bei 4000 Neuzugängen die ersten
    101 Übereinstimmungen im Hash.
    102 
    103 Der Hash ist also nicht gut genug, weil man sehr schnell und sehr häufig, diese
    104 Übereinstimmungen feststellen und behandeln müsste, wenn man weiterhin eindeutige
    105 Zuordnungen treffen will.
    106 
    107 Wenn man die Ausgabe der Hashfunktion auf 6 Bytes erweitert, kommen die ersten Kollisionen
    108 erst bei etwa 20 Mio erzeugten Hashes (Fingerabdrücken) und die kann man dann ohne
    109 Einbußen gesondert behandeln (Salzen), weil es so selten passiert.
    110 
    111 Übrigens, wenn man beim Menschen den Daumenabdruck nimmt und sich dabei auf 12 Merkmale
    112 beschränkt und sehr lockere Toleranzen ansetzt (was man in der Praxis nicht macht),
    113 hat man bereits nach 14000 Menschen eine Übereinstimmung. Bei 24 Merkmalen und sehr
    114 lockeren Toleranzen, hat man bei etwa nach 9 Mio. Menschen eine ungefähre Übereinstimmung.
    115 Da muss die Polizei schon sehr schlampig arbeiten, damit man fälschlicherweise beschuldigt wird.
    116 Die Zahlen in der Realität sind sogar noch deutlich höher.
    117 
    118 ** FlexoEntity
    119 
    120 Nun haben wir gesehen, dass wir mit der FlexOID (mit 6-Byte Hash) sehr viele unterschiedliche
    121 Dinge eindeutig bestimmen können. Da unsere FlexOID erstmal nur eine Zeichenfolge ist,
    122 brauchen wir etwas das damit umgehen kann und was dafür verantwortlich ist. Das ist die FlexoEntity.
    123 
    124 Sie beinhaltet zusätzlich eine Signatur und ein Origin-Feld, wo festgehalten wird, woher diese
    125 Entität stammt (beispielsweise aus einer Hashkollision oder einer Änderung an den weiteren Daten)
    126 
    127 Jede Klasse, die von FlexoEntity erbt, muss zwingend die Method "text_seed" implementieren, mit der
    128 der Algorithmus einer Hash-Funktion gefüttert wird, aus der dann der 6-Byte Hash herauspurzelt.
    129 Hashfunktionen sind z.B. MD5, SHA1, SHA256 oder wie von mir genutzt Blake2s.
    130 Die Mathematik dahinter ist recht aufwändig, aber wer sich mal einlesen möchte
    131 
    132 - Hashing in Smalltalk: Theory and Practice von Andres Valloud
    133 
    134 Damit die Hash-Funktion genug Eingabedaten pro Entity hat, muss man sich überlegen, welche Merkmale
    135 einer Entität man durch "text_seed" übermittelt.
    136 
    137 ** text_seed
    138 
    139 Man kann beliebige Klassen von FlexoEntity ableiten und erhält ohne Aufwand die Funktionalität
    140 zur eindeutigen Identifizierung und zur Lebenszyklus-Verwaltung
    141 
    142 
    143 Das ist das Beispiel einer OptionQuestion, also einer Testfrage, wo mögliche Antworten enthalten sind
    144 
    145     @property
    146     def text_seed(self) -> str:
    147         """Include answer options (and points) for deterministic ID generation."""
    148         base = super().text_seed
    149         if not self.options:
    150             return base
    151 
    152         joined = "|".join(
    153             f"{opt.text.strip()}:{opt.points}"
    154             for opt in sorted(self.options, key=lambda o: o.text.strip().lower())
    155         )
    156         return f"{base}::{joined}"
    157 
    158 ** Lebenszyklus
    159 
    160 Der Lebenszyklus einer Entität folgt dieser Reihenfolge und ist nicht umkehrbar
    161 
    162 - Entwurf (DRAFT)
    163 - Genehmigt (APPROVED)
    164 - Unterschrieben (SIGNED)
    165 - Veröffentlicht (PUBLISHED)
    166 - Veraltet (OBSOLETE)
    167 
    168 Eine Entität, die bereits die Stufe Veröffentlicht erreicht hat, kann nicht in die Stufe
    169 (nur) Unterschrieben zurückkehren. Daher ist auch die Lebenszyklusstufe in der ID kodiert
    170 (letztes Symbol der FlexOID)
    171 
    172 Beispiele für Entitäten:
    173 
    174 - Testfrage
    175 - Fragenkatalog
    176 - Einstufungstest
    177 - Zertifikat
    178 
    179 Ein veröffentlichter Einstufungstest kann nur Fragen beinhalten, die ihrerseits die Stufe Veröffentlicht
    180 erreicht haben. Ein Zertifikat kann nur ausgestellt werden, wenn der passende Einstufungstest die Stufe
    181 Veröffentlicht hat. Das origin-Feld des Zertifikats sollte sinnvollerweise die ID des Tests enthalten.
    182 
    183 ** Erhöhung der Versionsnummer oder neue ID
    184 
    185 Sobald - aus Gründen - eine neue ID vergeben werden muss, wird ggf. die Ursprungs-ID
    186 im Feld origin der neuen FlexoEntity gespeichert. So ist immer ein Suchen im Stammbaum möglich.
    187 
    188 AF-Q251022-70F759@001D
    189 │  │ │       │     │ │
    190 │  │ │       │     │ └── State (Draft, Approved, Signed, Published, Obsolete)
    191 │  │ │       │     └──── Version
    192 │  │ │       └────────── Hash (3 bytes, 6 Stellen)
    193 │  │ └────────────────── Date (YYMMDD)
    194 │  └──────────────────── Entity type (Question)
    195 └─────────────────────── Domain (e.g. Air force)
    196 
    197196
    198197Eine einfache Erhöhung der Versionsnummer (am Ende der ID) ist unter Umständen auch ausreichend,
  • tests/conftest.py

    r376e21b rd7499ca  
    11# tests/stubs/single_choice_question.py
     2from dataclasses import dataclass, field
    23import pytest
     4import platform
     5from pathlib import Path
    36from datetime import datetime
    4 from dataclasses import dataclass, field
    57from typing import List
    6 from flexoentity import FlexOID, FlexoEntity, EntityType, EntityState, Domain
     8from flexoentity import FlexOID, FlexoEntity, EntityType, EntityState, Domain, get_signing_backend, CertificateReference
     9
    710
    811@pytest.fixture
     
    98101    q._update_fingerprint()
    99102    return q
     103
     104SYSTEM = platform.system()
     105
     106
     107# ─────────────────────────────────────────────────────────────
     108# Basic test data directory + PEM test files
     109# ─────────────────────────────────────────────────────────────
     110
     111@pytest.fixture(scope="session")
     112def test_data_dir():
     113    return Path(__file__).parent / "data"
     114
     115
     116@pytest.fixture(scope="session")
     117def test_cert(test_data_dir):
     118    return test_data_dir / "testcert.pem"
     119
     120
     121@pytest.fixture(scope="session")
     122def test_key(test_data_dir):
     123    return test_data_dir / "testkey.pem"
     124
     125
     126# ─────────────────────────────────────────────────────────────
     127# CertificateReference fixtures for each platform
     128# ─────────────────────────────────────────────────────────────
     129
     130@pytest.fixture(scope="session")
     131def cert_ref_linux(test_cert, test_key):
     132    """Linux: Uses OpenSSL CMS with PEM cert + PEM private key."""
     133    return CertificateReference(
     134        platform="LINUX",
     135        identifier=str(test_cert),
     136        private_key_path=str(test_key),
     137        public_cert_path=str(test_cert),
     138    )
     139
     140
     141@pytest.fixture(scope="session")
     142def cert_ref_macos(test_cert):
     143    """
     144    macOS: Uses Keychain identity with Common Name (CN).
     145    The test cert must be imported into the login keychain with CN=FlexOSignerTest.
     146    """
     147    return CertificateReference(
     148        platform="MACOS",
     149        identifier="FlexOSignerTest",
     150        public_cert_path=str(test_cert),
     151    )
     152
     153@pytest.fixture(scope="session")
     154def backend(test_cert, test_key):
     155    """Return the correct backend for the current platform."""
     156
     157    if SYSTEM == "Linux":
     158        cert_ref = CertificateReference(
     159            platform="LINUX",
     160            identifier=str(test_cert),
     161            private_key_path=str(test_key),
     162            public_cert_path=str(test_cert),
     163        )
     164
     165    elif SYSTEM == "Darwin":
     166        cert_ref = CertificateReference(
     167            platform="MACOS",
     168            identifier="FlexOSignerTest",
     169            public_cert_path=str(test_cert),
     170        )
     171
     172    elif SYSTEM == "Windows":
     173        pytest.skip("Windows signing tests not implemented yet")
     174
     175    else:
     176        pytest.skip(f"Unsupported platform: {SYSTEM}")
     177
     178    try:
     179        backend = get_signing_backend(cert_ref)
     180        # sanity check: ensures cert exists and command is available
     181        _ = backend.certificate_thumbprint
     182        return backend
     183    except Exception as e:
     184        pytest.skip(f"Backend unavailable or misconfigured: {e}")
Note: See TracChangeset for help on using the changeset viewer.