Changeset d7499ca in flexoentity
- Timestamp:
- 11/24/25 15:17:00 (7 weeks ago)
- Branches:
- master
- Children:
- 2fd0536
- Parents:
- 376e21b
- Files:
-
- 8 added
- 5 edited
- 1 moved
-
README.md (modified) (1 diff)
-
flexoentity/__init__.py (modified) (2 diffs)
-
flexoentity/domain.py (modified) (1 diff)
-
flexoentity/flexo_entity.py (modified) (1 diff)
-
flexoentity/flexo_signature.py (added)
-
flexoentity/signing_backends.py (added)
-
org/FlexoEntityTalk.org (moved) (moved from org/FlexoEntity.org ) (2 diffs)
-
org/README.org (added)
-
org/certificates.org (added)
-
tests/conftest.py (modified) (2 diffs)
-
tests/data/test.p12 (added)
-
tests/data/testcert.pem (added)
-
tests/data/testkey.pem (added)
-
tests/test_signing.py (added)
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 10 27 (Flex-O-Grader, Flex-O-Vault, Flex-O-Drill, …). 11 28 12 It defines how entities such as questions, media, catalogs, and exams are * identified, versioned, signed, and verified* — all without any external database dependencies.29 It defines how entities such as questions, media, catalogs, and exams are **identified, versioned, signed, and verified** — all without any external database dependencies. 13 30 14 31 At its heart lie two modules: 15 32 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** 18 35 19 36 Together, they form a compact yet powerful foundation for audit-ready, reproducible data structures across offline and air-gapped deployments. 20 37 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 92 Each 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. “Air Force”)</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> 65 206 66 207 Transitions 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 211 Only 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 76 222 77 223 A lightweight immutable class representing the full identity of an entity. 78 224 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 86 237 Abstract base class for all versioned entities (e.g., Question, Exam, Catalog). 87 238 88 239 Implements: 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 93 245 94 246 Subclasses define a single property: 95 247 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 103 256 104 257 Each entity can self-verify its integrity: 105 258 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 265 If the file is tampered with (e.g. “Ohm’s” → “Omm’s”), verification fails: 266 267 268 <a id="orge08e5d3"></a> 269 270 # Real World Example 271 272 Below you can see the implementation of a dedicated FlexoEntity class, used for Domains. 273 We set an ENTITY<sub>TYPE</sub> and define the needed fields in the data class. We define how to create 274 a default object, the text<sub>seed</sub> (it is easy because the domain id is unique and therefore sufficient) 275 and 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 145 373 { 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 “flexo<sub>id</sub>”: “GEN<sub>GENERIC</sub>-D251124-29CE0F4BE59D@001S”, 375 “fingerprint”: “534BD2EC5C5511F1”, 376 “origin”: “GEN<sub>GENERIC</sub>-D251124-67C2CAE292CE@001D”, 377 “originator<sub>id</sub>”: “00000000-0000-0000-0000-000000000000”, 378 “owner<sub>id</sub>”: “00000000-0000-0000-0000-000000000000”, 379 “domain<sub>id</sub>”: “GEN<sub>GENERIC</sub>”, 380 “fullname”: “Generic Domain”, 381 “description”: “”, 382 “classification”: “UNCLASSIFIED” 153 383 } 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 565 No 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 199 577 200 578 All 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 204 586 MIT License 2025 205 Part of the * Flex-O family* by Flex-O-Dyne GmbH587 Part of the **Flex-O family** by Flex-O-Dyne GmbH 206 588 Designed for reproducible, audit-ready, human-centered software. 589 -
flexoentity/__init__.py
r376e21b rd7499ca 15 15 from .flexo_collection import FlexoCollection 16 16 from .domain import Domain 17 from .flexo_signature import FlexoSignature, CertificateReference 18 from .signing_backends import get_signing_backend 17 19 18 20 __all__ = [ … … 24 26 "EntityState", 25 27 "FlexoCollection", 28 "FlexoSignature", 29 "CertificateReference", 30 "get_signing_backend", 26 31 "logger" 27 32 ] -
flexoentity/domain.py
r376e21b rd7499ca 1 1 from uuid import UUID 2 2 from dataclasses import dataclass 3 from flexoentity import FlexOID, FlexoEntity, EntityType , EntityState3 from flexoentity import FlexOID, FlexoEntity, EntityType 4 4 5 5 @dataclass -
flexoentity/flexo_entity.py
r376e21b rd7499ca 87 87 CONFIG = "F" 88 88 EVENT = "E" 89 ATTESTATION = "X" 89 90 90 91 @classmethod -
org/FlexoEntityTalk.org
r376e21b rd7499ca 71 71 enthält, um möglichst eindeutig, aber nicht zu lang zu sein. 72 72 73 AF-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 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 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 Methode "text_seed" implementieren, 128 mit der 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 Das 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 159 Der 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 167 Eine 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 171 Beispiele für Entitäten: 172 173 - Testfrage 174 - Fragenkatalog 175 - Einstufungstest 176 - Zertifikat 177 178 Ein veröffentlichter Einstufungstest kann nur Fragen beinhalten, die ihrerseits die Stufe Veröffentlicht 179 erreicht haben. Ein Zertifikat kann nur ausgestellt werden, wenn der passende Einstufungstest die Stufe 180 Veröffentlicht hat. Das origin-Feld des Zertifikats sollte sinnvollerweise die ID des Tests enthalten. 181 182 ** Erhöhung der Versionsnummer oder neue ID 183 184 Sobald - aus Gründen - eine neue ID vergeben werden muss, wird ggf. die Ursprungs-ID 185 im Feld origin der neuen FlexoEntity gespeichert. So ist immer ein Suchen im Stammbaum möglich. 186 73 187 AF-Q251022-70F759@001D 74 188 │ │ │ │ │ │ … … 80 194 └─────────────────────── Domain (e.g. Air force) 81 195 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-Paradoxon86 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 Klasse89 betreten bis sich zwei finden, die mit mehr als 50 Prozent Wahrscheinlichkeit am gleichen90 Tag Geburtstag (also ein identisches Merkmal) haben?91 92 ** Lösung93 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 Prozent97 98 Unsere 3 Bytes ergeben zwar 16.000.000 mögliche Varianten - im Gegenzug zu den 365 Tagen im Jahr99 aus dem Geburtstagsbeispiel - aber da die Wahrscheinlichkeit zur Kollision hier bei100 etwa Wurzel 16 Mio. liegt, bekommt man bereits bei 4000 Neuzugängen die ersten101 Übereinstimmungen im Hash.102 103 Der Hash ist also nicht gut genug, weil man sehr schnell und sehr häufig, diese104 Übereinstimmungen feststellen und behandeln müsste, wenn man weiterhin eindeutige105 Zuordnungen treffen will.106 107 Wenn man die Ausgabe der Hashfunktion auf 6 Bytes erweitert, kommen die ersten Kollisionen108 erst bei etwa 20 Mio erzeugten Hashes (Fingerabdrücken) und die kann man dann ohne109 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 Merkmale112 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 sehr114 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 ** FlexoEntity119 120 Nun haben wir gesehen, dass wir mit der FlexOID (mit 6-Byte Hash) sehr viele unterschiedliche121 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 diese125 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 der128 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öchte131 132 - Hashing in Smalltalk: Theory and Practice von Andres Valloud133 134 Damit die Hash-Funktion genug Eingabedaten pro Entity hat, muss man sich überlegen, welche Merkmale135 einer Entität man durch "text_seed" übermittelt.136 137 ** text_seed138 139 Man kann beliebige Klassen von FlexoEntity ableiten und erhält ohne Aufwand die Funktionalität140 zur eindeutigen Identifizierung und zur Lebenszyklus-Verwaltung141 142 143 Das ist das Beispiel einer OptionQuestion, also einer Testfrage, wo mögliche Antworten enthalten sind144 145 @property146 def text_seed(self) -> str:147 """Include answer options (and points) for deterministic ID generation."""148 base = super().text_seed149 if not self.options:150 return base151 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 ** Lebenszyklus159 160 Der Lebenszyklus einer Entität folgt dieser Reihenfolge und ist nicht umkehrbar161 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 Stufe169 (nur) Unterschrieben zurückkehren. Daher ist auch die Lebenszyklusstufe in der ID kodiert170 (letztes Symbol der FlexOID)171 172 Beispiele für Entitäten:173 174 - Testfrage175 - Fragenkatalog176 - Einstufungstest177 - Zertifikat178 179 Ein veröffentlichter Einstufungstest kann nur Fragen beinhalten, die ihrerseits die Stufe Veröffentlicht180 erreicht haben. Ein Zertifikat kann nur ausgestellt werden, wenn der passende Einstufungstest die Stufe181 Veröffentlicht hat. Das origin-Feld des Zertifikats sollte sinnvollerweise die ID des Tests enthalten.182 183 ** Erhöhung der Versionsnummer oder neue ID184 185 Sobald - aus Gründen - eine neue ID vergeben werden muss, wird ggf. die Ursprungs-ID186 im Feld origin der neuen FlexoEntity gespeichert. So ist immer ein Suchen im Stammbaum möglich.187 188 AF-Q251022-70F759@001D189 │ │ │ │ │ │190 │ │ │ │ │ └── State (Draft, Approved, Signed, Published, Obsolete)191 │ │ │ │ └──── Version192 │ │ │ └────────── Hash (3 bytes, 6 Stellen)193 │ │ └────────────────── Date (YYMMDD)194 │ └──────────────────── Entity type (Question)195 └─────────────────────── Domain (e.g. Air force)196 197 196 198 197 Eine einfache Erhöhung der Versionsnummer (am Ende der ID) ist unter Umständen auch ausreichend, -
tests/conftest.py
r376e21b rd7499ca 1 1 # tests/stubs/single_choice_question.py 2 from dataclasses import dataclass, field 2 3 import pytest 4 import platform 5 from pathlib import Path 3 6 from datetime import datetime 4 from dataclasses import dataclass, field5 7 from typing import List 6 from flexoentity import FlexOID, FlexoEntity, EntityType, EntityState, Domain 8 from flexoentity import FlexOID, FlexoEntity, EntityType, EntityState, Domain, get_signing_backend, CertificateReference 9 7 10 8 11 @pytest.fixture … … 98 101 q._update_fingerprint() 99 102 return q 103 104 SYSTEM = platform.system() 105 106 107 # ───────────────────────────────────────────────────────────── 108 # Basic test data directory + PEM test files 109 # ───────────────────────────────────────────────────────────── 110 111 @pytest.fixture(scope="session") 112 def test_data_dir(): 113 return Path(__file__).parent / "data" 114 115 116 @pytest.fixture(scope="session") 117 def test_cert(test_data_dir): 118 return test_data_dir / "testcert.pem" 119 120 121 @pytest.fixture(scope="session") 122 def 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") 131 def 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") 142 def 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") 154 def 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.
