Index: README.md
===================================================================
--- README.md	(revision 376e21b3865f5979339d98773fc106b2673d35a9)
+++ README.md	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -1,206 +1,589 @@
-#+TITLE: flexoentity
-#+SUBTITLE: A hardened entity base and deterministic identifier system for the Flex-O family
-#+AUTHOR: Enno
-#+DATE: 2025-10-20
-#+OPTIONS: toc:3 num:nil
-
-* Overview
-
-`flexoentity` provides the *identity and lifecycle backbone* for all Flex-O components  
+
+# Table of Contents
+
+-   [Overview](#org8360697)
+-   [Flex-O ID Structure](#org89396ad)
+-   [Lifecycle States](#orgd522c8c)
+-   [Core Classes](#org381a3f6)
+    -   [FlexOID](#org6abdc86)
+    -   [`FlexoEntity`](#org15b9543)
+-   [Integrity Verification](#org1617277)
+-   [Real World Example](#orge08e5d3)
+-   [Usage](#orgc8e7dd2)
+-   [Serialization Example](#org1b3bbed)
+-   [Entity Type and State Codes](#org5dfc109)
+-   [Design Notes](#org659c733)
+-   [Dependencies](#orga67f3a6)
+-   [Integration](#org10ab165)
+-   [License](#org48ba119)
+
+
+
+<a id="org8360697"></a>
+
+# Overview
+
+\`flexoentity\` provides the **identity and lifecycle backbone** for all Flex-O components  
 (Flex-O-Grader, Flex-O-Vault, Flex-O-Drill, …).
 
-It defines how entities such as questions, media, catalogs, and exams are *identified, versioned, signed, and verified* — all without any external database dependencies.
+It defines how entities such as questions, media, catalogs, and exams are **identified, versioned, signed, and verified** — all without any external database dependencies.
 
 At its heart lie two modules:
 
-- =id_factory.py= – robust, cryptographically-verifiable *Flex-O ID generator*
-- =versioned_entity.py= – abstract *base class for all versioned entities*
+-   `id_factory.py` – robust, cryptographically-verifiable **Flex-O ID generator**
+-   `flexo_entity.py` – abstract **base class for all versioned entities**
 
 Together, they form a compact yet powerful foundation for audit-ready, reproducible data structures across offline and air-gapped deployments.
 
-✳️ Design Goals
-|----------------|--------------------------------------------------------------------------------------------------------|
-| Goal           | Description                                                                                            |
-|----------------|--------------------------------------------------------------------------------------------------------|
-| *Determinism*  | IDs are derived from canonicalized entity content — identical input always yields identical ID prefix. |
-| *Integrity*    | BLAKE2s hashing and digital signatures protect against manual tampering.                               |
-| *Traceability* | Version numbers (=@001A=, =@002S=, …) track entity lifecycle transitions.                              |
-| *Stability*    | Hash prefixes remain constant across state changes; only version and state suffixes evolve.            |
-| *Auditability* | Every entity can be serialized, verified, and reconstructed without hidden dependencies.               |
-| *Simplicity*   | Pure-Python, zero external libraries, self-contained and easy to embed.                                |
-|----------------|--------------------------------------------------------------------------------------------------------|
-
-* Flex-O ID Structure
-
-Each entity carries a unique *Flex-O ID*, generated by =FlexOID.generate()=.
-
-#+BEGIN_EXAMPLE
-AF-Q250101-9A4C2D@003S
-#+END_EXAMPLE
-
-|-----------|----------|---------------------------------------------|
-| Segment   | Example  | Meaning                                     |
-|-----------|----------|---------------------------------------------|
-| *Domain*  | =AF=     | Logical scope (e.g. "Air Force")            |
-| *Type*    | =Q=      | Entity type (e.g. Question)                 |
-| *Date*    | =250101= | UTC creation date (YYMMDD)                  |
-| *Hash*    | =9A4C2D= | 6-digit BLAKE2s digest of canonical content |
-| *Version* | =003=    | Sequential version counter                  |
-| *State*   | =S=      | Lifecycle state (=D=, =A=, =S=, =P=, =O=)   |
-|-----------|----------|---------------------------------------------|
-
-Hash collisions within a single session are automatically disambiguated (=-1=, =-2=, …).
-
-* Lifecycle States
-
-|-----------------------|---------|-----------------------------|
-| State                 | Abbrev. | Description                 |
-|-----------------------|---------|-----------------------------|
-| *DRAFT*               | =D=     | Editable, not yet validated |
-| *APPROVED*            | =A=     | Reviewed and accepted       |
-| *APPROVED_AND_SIGNED* | =S=     | Cryptographically signed    |
-| *PUBLISHED*           | =P=     | Released to consumers       |
-| *OBSOLETE*            | =O=     | Archived or replaced        |
-|-----------------------|---------|-----------------------------|
+-   Design Goals
+
+<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">
+
+
+<colgroup>
+<col  class="org-left" />
+
+<col  class="org-left" />
+</colgroup>
+<thead>
+<tr>
+<th scope="col" class="org-left">Goal</th>
+<th scope="col" class="org-left">Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td class="org-left"><b>Determinism</b></td>
+<td class="org-left">IDs are derived from canonicalized entity content — identical input always yields identical ID prefix.</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>Integrity</b></td>
+<td class="org-left">BLAKE2s hashing and digital signatures protect against manual tampering.</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>Traceability</b></td>
+<td class="org-left">Version numbers (<code>@001A</code>, <code>@002S</code>, …) track entity lifecycle transitions.</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>Stability</b></td>
+<td class="org-left">Hash prefixes remain constant across state changes; only version and state suffixes evolve.</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>Auditability</b></td>
+<td class="org-left">Every entity can be serialized, verified, and reconstructed without hidden dependencies.</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>Simplicity</b></td>
+<td class="org-left">Pure-Python, zero external libraries, self-contained and easy to embed.</td>
+</tr>
+</tbody>
+</table>
+
+
+<a id="org89396ad"></a>
+
+# Flex-O ID Structure
+
+Each entity carries a unique **Flex-O ID**, generated by `FlexOID.generate()`.
+
+    AF-I250101-9A4C2D@003S
+
+<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">
+
+
+<colgroup>
+<col  class="org-left" />
+
+<col  class="org-left" />
+
+<col  class="org-left" />
+</colgroup>
+<thead>
+<tr>
+<th scope="col" class="org-left">Segment</th>
+<th scope="col" class="org-left">Example</th>
+<th scope="col" class="org-left">Meaning</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td class="org-left"><b>Domain</b></td>
+<td class="org-left"><code>AF or PY_LANG</code></td>
+<td class="org-left">Uppercase - Logical scope (e.g. &ldquo;Air Force&rdquo;)</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>Type</b></td>
+<td class="org-left"><code>I</code></td>
+<td class="org-left">Entity type (e.g. ITEM)</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>Date</b></td>
+<td class="org-left"><code>250101</code></td>
+<td class="org-left">UTC creation date (YYMMDD)</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>Hash</b></td>
+<td class="org-left"><code>9A4C2D4F6E53</code></td>
+<td class="org-left">12-digit BLAKE2s digest of canonical content</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>Version</b></td>
+<td class="org-left"><code>003</code></td>
+<td class="org-left">Sequential version counter</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>State</b></td>
+<td class="org-left"><code>S</code></td>
+<td class="org-left">Lifecycle state (D, A, S, P, O)</td>
+</tr>
+</tbody>
+</table>
+
+
+<a id="orgd522c8c"></a>
+
+# Lifecycle States
+
+<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">
+
+
+<colgroup>
+<col  class="org-left" />
+
+<col  class="org-left" />
+
+<col  class="org-left" />
+</colgroup>
+<thead>
+<tr>
+<th scope="col" class="org-left">State</th>
+<th scope="col" class="org-left">Abbrev.</th>
+<th scope="col" class="org-left">Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td class="org-left"><b>DRAFT</b></td>
+<td class="org-left"><code>D</code></td>
+<td class="org-left">Editable, not yet validated</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>APPROVED</b></td>
+<td class="org-left"><code>A</code></td>
+<td class="org-left">Reviewed and accepted</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>APPROVED<sub>AND</sub><sub>SIGNED</sub></b></td>
+<td class="org-left"><code>S</code></td>
+<td class="org-left">Cryptographically signed</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>PUBLISHED</b></td>
+<td class="org-left"><code>P</code></td>
+<td class="org-left">Released to consumers</td>
+</tr>
+
+<tr>
+<td class="org-left"><b>OBSOLETE</b></td>
+<td class="org-left"><code>O</code></td>
+<td class="org-left">Archived or replaced</td>
+</tr>
+</tbody>
+</table>
 
 Transitions follow a strict progression:
-#+BEGIN_EXAMPLE
-DRAFT → APPROVED → APPROVED_AND_SIGNED → PUBLISHED → OBSOLETE
-#+END_EXAMPLE
-
-Version increments occur automatically for all *stable* transitions (APPROVED, SIGNED, PUBLISHED).
-
-* Core Classes
-
-** =FlexOID=
+
+    DRAFT -> APPROVED -> APPROVED_AND_SIGNED -> PUBLISHED -> OBSOLETE
+
+Only DRAFT entities can be deleted - all others got OBSOLETE mark instead
+
+
+<a id="org381a3f6"></a>
+
+# Core Classes
+
+
+<a id="org6abdc86"></a>
+
+## FlexOID
 
 A lightweight immutable class representing the full identity of an entity.
 
-*Highlights*
-- =generate(domain, entity_type, estate, text, version=1)= → create a new ID
-- =next_version(oid)= → increment version safely
-- =clone_new_base(domain, entity_type, estate, text)= → start a new lineage
-- Deterministic prefix, state-dependent signature
-
-** =FlexoEntity=
+**Highlights**
+
+-   safe<sub>generate</sub>(domain, entity<sub>type</sub>, estate, text, version=1, repo) -> create a new ID
+-   next<sub>version</sub>(oid) -> increment version safely
+-   clone<sub>new</sub><sub>base</sub>(domain, entity<sub>type</sub>, estate, text) -> start a new lineage
+-   Deterministic prefix, state-dependent signature
+
+
+<a id="org15b9543"></a>
+
+## `FlexoEntity`
+
 Abstract base class for all versioned entities (e.g., Question, Exam, Catalog).
 
 Implements:
-- ID lifecycle management (=approve()=, =sign()=, =publish()=, =obsolete()=)
-- JSON serialization (=to_json()=, =from_json()=)
-- Integrity verification (=verify_integrity(entity)=)
-- Controlled state transitions with automatic timestamps
+
+-   ID lifecycle management (approve(), sign(), publish(), obsolete())
+-   Serialization (to<sub>json</sub>(), from<sub>json</sub>(), to<sub>dict</sub>(), from<sub>dict</sub>())
+-   Integrity verification (verify<sub>integrity</sub>(entity))
+-   Controlled state transitions with automatic timestamps
 
 Subclasses define a single property:
 
-#+BEGIN_SRC python
-@property
-def text_seed(self) -> str:
-    """Canonical text or core content for hashing."""
-#+END_SRC
-
-* Integrity Verification
+    @property
+    def text_seed(self) -> str:
+        """Canonical text or core content for hashing."""
+
+
+<a id="org1617277"></a>
+
+# Integrity Verification
 
 Each entity can self-verify its integrity:
 
-#+BEGIN_SRC python
-entity = Question("AF", EntityType.ITEM, "What is Ohm’s law?")
-json_str = entity.to_json()
-reloaded = Question.from_json(json_str)
-
-assert FlexoEntity.verify_integrity(reloaded)
-#+END_SRC
-
-If the file is tampered with (e.g. "Ohm’s" → "Omm’s"), verification fails:
-
-#+BEGIN_SRC python
-assert not FlexoEntity.verify_integrity(reloaded)
-#+END_SRC
-
-* Example
-#+BEGIN_SRC python
-from flexoentity.versioned_entity import FlexoEntity, EntityType, EntityState
-
-class Question(FlexoEntity):
-    def __init__(self, domain, text):
-        self._text = text
-        super().__init__(domain, EntityType.ITEM)
-
-    @property
-    def text_seed(self):
-        return self._text
-
-* Usage
-#+BEGIN_SRC python
-q = Question("AF", "What is Ohm’s law?")
-print(q.flexo_id)             # AF-Q250101-9A4C2D@001D
-q.approve()
-print(q.flexo_id)             # AF-Q250101-9A4C2D@002A
-q.sign()
-print(q.flexo_id)             # AF-Q250101-9A4C2D@003S
-#+END_SRC
-
-* JSON Example
-#+BEGIN_SRC json
+    entity = Question.with_domain_id(domain_id="AF", text="What is Ohm’s law?", topic="Electronics")
+    json_str = entity.to_json()
+    reloaded = Question.from_json(json_str)
+    
+    assert FlexoEntity.verify_integrity(reloaded)
+
+If the file is tampered with (e.g. &ldquo;Ohm’s&rdquo; → &ldquo;Omm’s&rdquo;), verification fails:
+
+
+<a id="orge08e5d3"></a>
+
+# Real World Example
+
+Below you can see the implementation of a dedicated FlexoEntity class, used for Domains.
+We set an ENTITY<sub>TYPE</sub> and define the needed fields in the data class. We define how to create
+a default object, the text<sub>seed</sub> (it is easy because the domain id is unique and therefore sufficient)
+and the methods for serialization.
+
+    from uuid import UUID
+    from dataclasses import dataclass
+    from flexoentity import FlexOID, FlexoEntity, EntityType
+    
+    @dataclass
+    class Domain(FlexoEntity):
+        """
+        I am a helper class to provide more information than just a
+        domain abbreviation in FlexOID, doing mapping and management
+        """
+    
+        ENTITY_TYPE = EntityType.DOMAIN
+    
+        fullname: str = ""
+        description: str = ""
+        classification: str = "UNCLASSIFIED"
+    
+        @classmethod
+        def default(cls):
+            """Return the default domain object."""
+            return cls.with_domain_id(domain_id="GEN_GENERIC",
+                                      fullname="Generic Domain", classification="UNCLASSIFIED")
+    
+        @property
+        def text_seed(self) -> str:
+            return self.domain_id
+    
+        def to_dict(self):
+            base = super().to_dict()
+            base.update({
+                "flexo_id": self.flexo_id,
+                "domain_id": self.domain_id,
+                "fullname": self.fullname,
+                "description": self.description,
+                "classification": self.classification,
+            })
+            return base
+    
+        @classmethod
+        def from_dict(cls, data):
+            # Must have flexo_id
+            if "flexo_id" not in data:
+                raise ValueError("Domain serialization missing 'flexo_id'.")
+    
+            flexo_id = FlexOID(data["flexo_id"])
+    
+            obj = cls(
+                fullname=data.get("fullname", ""),
+                description=data.get("description", ""),
+                classification=data.get("classification", "UNCLASSIFIED"),
+                flexo_id=flexo_id,
+                _in_factory=True
+            )
+    
+            # Restore metadata
+            obj.origin = data.get("origin")
+            obj.fingerprint = data.get("fingerprint", "")
+            obj.originator_id = (
+                UUID(data["originator_id"]) if data.get("originator_id") else None
+            )
+            obj.owner_id = (
+                UUID(data["owner_id"]) if data.get("owner_id") else None
+            )
+    
+            return obj
+
+
+<a id="orgc8e7dd2"></a>
+
+# Usage
+
+    d = Domain.default()
+    print(d.flexo_id)             # GEN_GENERIC-D251124-67C2CAE292CE@001D
+    d.approve()
+    print(d.flexo_id)             # GEN_GENERIC-D251124-67C2CAE292CE@001A
+    d.sign()
+    print(d.flexo_id)             # GEN_GENERIC-D251124-67C2CAE292CE@001S
+
+
+<a id="org1b3bbed"></a>
+
+# Serialization Example
+
+    {
+        'flexo_id': FlexOID(GEN_GENERIC-D251124-29CE0F4BE59D@001S),
+        'fingerprint': '534BD2EC5C5511F1',
+        'origin': FlexOID(GEN_GENERIC-D251124-67C2CAE292CE@001D),
+        'originator_id': '00000000-0000-0000-0000-000000000000',
+        'owner_id': '00000000-0000-0000-0000-000000000000',
+        'domain_id': 'GEN_GENERIC',
+        'fullname': 'Generic Domain',
+        'description': '',
+        'classification': 'UNCLASSIFIED'}
+
+\#+BEGIN<sub>SRC</sub> js
+
 {
-  "domain": "AF",
-  "entity_type": "QUESTION",
-  "text_seed": "What is Ohm’s law?",
-  "state": "APPROVED_AND_SIGNED",
-  "version": 3,
-  "flexo_id": "AF-Q250101-9A4C2D@003S",
-  "signature": "1E3A9F2A8B7C4D11",
+        &ldquo;flexo<sub>id</sub>&rdquo;: &ldquo;GEN<sub>GENERIC</sub>-D251124-29CE0F4BE59D@001S&rdquo;,
+        &ldquo;fingerprint&rdquo;: &ldquo;534BD2EC5C5511F1&rdquo;,
+        &ldquo;origin&rdquo;: &ldquo;GEN<sub>GENERIC</sub>-D251124-67C2CAE292CE@001D&rdquo;,
+        &ldquo;originator<sub>id</sub>&rdquo;: &ldquo;00000000-0000-0000-0000-000000000000&rdquo;,
+        &ldquo;owner<sub>id</sub>&rdquo;: &ldquo;00000000-0000-0000-0000-000000000000&rdquo;,
+        &ldquo;domain<sub>id</sub>&rdquo;: &ldquo;GEN<sub>GENERIC</sub>&rdquo;,
+        &ldquo;fullname&rdquo;: &ldquo;Generic Domain&rdquo;,
+        &ldquo;description&rdquo;: &ldquo;&rdquo;,
+        &ldquo;classification&rdquo;: &ldquo;UNCLASSIFIED&rdquo;
 }
-#+END_SRC
-
-* Entity Type and State Codes
-
-|-------------|------|------------------------------|
-| EntityType  | Code | Typical Use                  |
-|-------------|------|------------------------------|
-| QUESTION    | Q    | Exam question                |
-| CATALOG     | CAT  | Question or media collection |
-| EXAM        | EX   | Composed test instance       |
-| DATABASE    | DB   | Persistent store             |
-| CERTIFICATE | CERT | Digital or approval document |
-|-------------|------|------------------------------|
-
-|---------------------|------|-------------------|
-| EntityState         | Code | Meaning           |
-|---------------------|------|-------------------|
-| DRAFT               | D    | Work in progress  |
-| APPROVED            | A    | Reviewed          |
-| APPROVED_AND_SIGNED | S    | Signed version    |
-| PUBLISHED           | P    | Publicly released |
-| OBSOLETE            | O    | Deprecated        |
-|---------------------|------|-------------------|
-
-* Design Notes
-- *Hash Stability:* Only domain, entity type, and content text influence the hash.
-  This ensures consistent prefixes across state changes.
-- *State-Dependent Signatures:* Each lifecycle stage has its own signature seed.
-  Modifying a file without re-signing invalidates integrity.
-- *Obsolescence Threshold:* Version numbers above 900 trigger warnings;
-  beyond 999 are considered obsolete.
-- *Clone Lineages:* Cloning an entity resets versioning but preserves metadata lineage.
-
-* Dependencies
-- Python 3.11+
-- Standard library only (=hashlib=, =json=, =datetime=, =enum=, =dataclasses=)
-
-No external packages. Fully compatible with *Guix*, *air-gapped* deployments, and *reproducible builds*.
-
-* Integration
-`flexoentity` is imported by higher-level modules such as:
-
-- *Flex-O-Grader* → manages question catalogs and exam bundles
-- *Flex-O-Vault* → provides persistent media storage with metadata integrity
-- *Flex-O-Drill* → uses versioned entities for training simulations
+
+
+<a id="org5dfc109"></a>
+
+# Entity Type and State Codes
+
+<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">
+
+
+<colgroup>
+<col  class="org-left" />
+
+<col  class="org-left" />
+
+<col  class="org-left" />
+</colgroup>
+<thead>
+<tr>
+<th scope="col" class="org-left">EntityType</th>
+<th scope="col" class="org-left">Code</th>
+<th scope="col" class="org-left">Typical Use</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td class="org-left">GENERIC</td>
+<td class="org-left">G</td>
+<td class="org-left">Generic entities that does not fit other types yet or are temporarily only</td>
+</tr>
+
+<tr>
+<td class="org-left">DOMAIN</td>
+<td class="org-left">D</td>
+<td class="org-left">Every Domain is of this type</td>
+</tr>
+
+<tr>
+<td class="org-left">MEDIA</td>
+<td class="org-left">M</td>
+<td class="org-left">Every media item belongs to this type, e.g. Pictures, Audio, Video</td>
+</tr>
+
+<tr>
+<td class="org-left">ITEM</td>
+<td class="org-left">I</td>
+<td class="org-left">An Entity what is usually used in a collection, e.g. Questions in a test</td>
+</tr>
+
+<tr>
+<td class="org-left">COLLECTION</td>
+<td class="org-left">C</td>
+<td class="org-left">A collection of items, as an Exam or a catalog</td>
+</tr>
+
+<tr>
+<td class="org-left">TEXT</td>
+<td class="org-left">T</td>
+<td class="org-left">A text document</td>
+</tr>
+
+<tr>
+<td class="org-left">HANDOUT</td>
+<td class="org-left">H</td>
+<td class="org-left">A published document</td>
+</tr>
+
+<tr>
+<td class="org-left">OUTPUT</td>
+<td class="org-left">O</td>
+<td class="org-left">The output of a computation</td>
+</tr>
+
+<tr>
+<td class="org-left">RECORD</td>
+<td class="org-left">R</td>
+<td class="org-left">Record type data, as bibliography entries</td>
+</tr>
+
+<tr>
+<td class="org-left">SESSION</td>
+<td class="org-left">S</td>
+<td class="org-left">A unique session, e.g. managed by a session manager</td>
+</tr>
+
+<tr>
+<td class="org-left">USER</td>
+<td class="org-left">U</td>
+<td class="org-left">User objects</td>
+</tr>
+
+<tr>
+<td class="org-left">CONFIG</td>
+<td class="org-left">F</td>
+<td class="org-left">CONFIG files that need to be tracked over time and state</td>
+</tr>
+
+<tr>
+<td class="org-left">EVENT</td>
+<td class="org-left">E</td>
+<td class="org-left">Events that have to be tracked over time, as status messages or orders</td>
+</tr>
+
+<tr>
+<td class="org-left">ATTESTATION</td>
+<td class="org-left">X</td>
+<td class="org-left">Entities that attest a formal technical (not human) check e.g. Signatures</td>
+</tr>
+</tbody>
+</table>
+
+<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">
+
+
+<colgroup>
+<col  class="org-left" />
+
+<col  class="org-left" />
+
+<col  class="org-left" />
+</colgroup>
+<thead>
+<tr>
+<th scope="col" class="org-left">EntityState</th>
+<th scope="col" class="org-left">Code</th>
+<th scope="col" class="org-left">Meaning</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td class="org-left">DRAFT</td>
+<td class="org-left">D</td>
+<td class="org-left">Work in progress</td>
+</tr>
+
+<tr>
+<td class="org-left">APPROVED</td>
+<td class="org-left">A</td>
+<td class="org-left">Reviewed</td>
+</tr>
+
+<tr>
+<td class="org-left">APPROVED<sub>AND</sub><sub>SIGNED</sub></td>
+<td class="org-left">S</td>
+<td class="org-left">Signed version</td>
+</tr>
+
+<tr>
+<td class="org-left">PUBLISHED</td>
+<td class="org-left">P</td>
+<td class="org-left">Publicly released</td>
+</tr>
+
+<tr>
+<td class="org-left">OBSOLETE</td>
+<td class="org-left">O</td>
+<td class="org-left">Deprecated</td>
+</tr>
+</tbody>
+</table>
+
+
+<a id="org659c733"></a>
+
+# Design Notes
+
+-   **Hash Stability:** Only domain, entity type, and content text influence the hash.
+    This ensures consistent prefixes across state changes.
+-   **State-Dependent Signatures:** Each lifecycle stage has its own signature seed.
+    Modifying a file without re-signing invalidates integrity.
+-   **Obsolescence Threshold:** Version numbers above 900 trigger warnings;
+    beyond 999 are considered obsolete.
+-   **Clone Lineages:** Cloning an entity resets versioning but preserves metadata lineage.
+
+
+<a id="orga67f3a6"></a>
+
+# Dependencies
+
+-   Python 3.11+
+-   Standard library only (`hashlib`, `json`, `datetime`, `enum`, `dataclasses`)
+
+No external packages. Fully compatible with **Guix**, **air-gapped** deployments, and **reproducible builds**.
+
+
+<a id="org10ab165"></a>
+
+# Integration
+
+\`flexoentity\` is imported by higher-level modules such as:
+
+-   **Flex-O-Grader** → manages question catalogs and exam bundles
+-   **Flex-O-Vault** → provides persistent media storage with metadata integrity
+-   **Flex-O-Drill** → uses versioned entities for training simulations
 
 All share the same identity and versioning logic — ensuring that
-*what was approved, signed, and published remains provably authentic.*
-
-* License
+**what was approved, signed, and published remains provably authentic.**
+
+
+<a id="org48ba119"></a>
+
+# License
+
 MIT License 2025
-Part of the *Flex-O family* by Flex-O-Dyne GmbH 
+Part of the **Flex-O family** by Flex-O-Dyne GmbH 
 Designed for reproducible, audit-ready, human-centered software.
+
Index: flexoentity/__init__.py
===================================================================
--- flexoentity/__init__.py	(revision 376e21b3865f5979339d98773fc106b2673d35a9)
+++ flexoentity/__init__.py	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -15,4 +15,6 @@
 from .flexo_collection import FlexoCollection
 from .domain import Domain
+from .flexo_signature import FlexoSignature, CertificateReference
+from .signing_backends import get_signing_backend
 
 __all__ = [
@@ -24,4 +26,7 @@
     "EntityState",
     "FlexoCollection",
+    "FlexoSignature",
+    "CertificateReference",
+    "get_signing_backend",
     "logger"
 ]
Index: flexoentity/domain.py
===================================================================
--- flexoentity/domain.py	(revision 376e21b3865f5979339d98773fc106b2673d35a9)
+++ flexoentity/domain.py	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -1,5 +1,5 @@
 from uuid import UUID
 from dataclasses import dataclass
-from flexoentity import FlexOID, FlexoEntity, EntityType, EntityState
+from flexoentity import FlexOID, FlexoEntity, EntityType
 
 @dataclass
Index: flexoentity/flexo_entity.py
===================================================================
--- flexoentity/flexo_entity.py	(revision 376e21b3865f5979339d98773fc106b2673d35a9)
+++ flexoentity/flexo_entity.py	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -87,4 +87,5 @@
     CONFIG = "F"
     EVENT = "E"
+    ATTESTATION = "X"
 
     @classmethod
Index: flexoentity/flexo_signature.py
===================================================================
--- flexoentity/flexo_signature.py	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
+++ flexoentity/flexo_signature.py	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -0,0 +1,99 @@
+import base64
+from dataclasses import dataclass
+from typing import Optional
+from uuid import UUID
+from flexoentity import FlexoEntity, FlexOID, EntityType
+
+@dataclass
+class CertificateReference:
+    """
+    A cross-platform reference to a signing certificate.
+
+    This does NOT contain private keys or secret material.
+    It only contains the information required for OS-provided APIs
+    (OpenSSL, certutil, security) to *locate* or *verify* the certificate.
+
+    Fields:
+      platform:
+         Optional override for backend creation.
+         Values: "LINUX", "WINDOWS", "MACOS" (case-insensitive).
+         If None, platform.system() determines the backend.
+
+      identifier:
+         Linux:   path to certificate PEM file
+         Windows: certificate thumbprint (SHA1, with or without colons)
+         macOS:   certificate Common Name (CN) in Keychain
+
+      private_key_path:
+         Linux only:
+            path to private key PEM file.
+         Windows/macOS:
+            MUST be None.
+
+      public_cert_path:
+         The path to the public certificate for verification.
+         Linux:
+            optional (defaults to `identifier`)
+         Windows:
+            optional; used if you want to validate against a specific cert
+         macOS:
+            REQUIRED (used for OpenSSL verification)
+    """
+
+    platform: Optional[str] = None
+    identifier: str = ""
+    private_key_path: Optional[str] = None
+    public_cert_path: Optional[str] = None
+
+@dataclass
+class FlexoSignature(FlexoEntity):
+    """
+    I represent a digital or procedural signature for another entity.
+
+    This is a stub version: I only carry logical metadata, not cryptographic proof.
+    Later, platform-specific implementations (e.g. Windows certificate signing)
+    can fill the 'signature_data' field with real data.
+
+    Lifecycle:
+      - Created in DRAFT → becomes APPROVED once verified
+      - Optionally moves to PUBLISHED if distributed externally
+    """
+    ENTITY_TYPE = EntityType.ATTESTATION
+
+    signed_entity: Optional[FlexOID] = None
+    signer_id: Optional[UUID] = None
+
+    # PKCS#7 DER, base64-encoded
+    signature_data: str = ""
+    signature_type: str = "PKCS7-DER"
+
+    certificate_reference: CertificateReference | None = None
+    certificate_thumbprint: str = ""
+
+    comment: Optional[str] = None
+
+    @property
+    def text_seed(self):
+        return f"{self.signed_entity}:{self.signer_id}:{self.certificate_thumbprint}"
+
+    @classmethod
+    def default(cls):
+        """Required by FlexoEntity. Returns an empty draft signature."""
+        return cls()
+
+    @classmethod
+    def create_signed(cls, data: bytes, entity: FlexOID, signer_id: UUID, backend):
+        sig = backend.sign(data)
+        return cls.with_domain_id(
+            domain_id=entity.domain_id,
+            signed_entity=entity,
+            signer_id=signer_id,
+            signature_data=base64.b64encode(sig).decode(),
+            certificate_reference=backend.cert_ref,
+            certificate_thumbprint=backend.certificate_thumbprint
+        )
+
+    def verify(self, data: bytes, backend) -> bool:
+        raw = base64.b64decode(self.signature_data)
+        return backend.verify(data, raw)
+
Index: flexoentity/signing_backends.py
===================================================================
--- flexoentity/signing_backends.py	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
+++ flexoentity/signing_backends.py	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -0,0 +1,214 @@
+import subprocess
+import platform
+from pathlib import Path
+import tempfile
+import uuid
+from abc import ABC, abstractmethod
+
+class SigningBackend(ABC):
+    def __init__(self, cert_ref):
+        """
+        cert_ref is an object that is expected to provide (depending on platform):
+
+        Linux:
+          - identifier: path to certificate (PEM)
+          - private_key_path: path to private key (PEM)
+          - public_cert_path: optional, defaults to identifier if missing
+
+        Windows:
+          - identifier: certificate thumbprint (SHA1; with or without colons)
+
+        macOS:
+          - identifier: certificate Common Name (CN)
+          - public_cert_path: path to public certificate (PEM)
+        """
+        self.cert_ref = cert_ref
+
+    @property
+    @abstractmethod
+    def certificate_thumbprint(self) -> str:
+        """Return canonical thumbprint for Signature entity storage."""
+        raise NotImplementedError
+
+    @abstractmethod
+    def sign(self, data: bytes) -> bytes:
+        """Return PKCS#7 DER signature bytes."""
+        raise NotImplementedError
+
+    @abstractmethod
+    def verify(self, data: bytes, signature: bytes) -> bool:
+        """Return True/False after verifying PKCS#7 signature."""
+        raise NotImplementedError
+
+    def _tmp(self, suffix: str) -> Path:
+        """Return a unique temporary file path for this process."""
+        return Path(tempfile.NamedTemporaryFile(delete=False).name).with_suffix(suffix)
+
+    
+class LinuxOpenSSLBackend(SigningBackend):
+
+    @property
+    def certificate_thumbprint(self) -> str:
+        out = subprocess.check_output([
+            "openssl", "x509", "-in", self.cert_ref.identifier,
+            "-noout", "-fingerprint", "-sha1"
+        ]).decode()
+        return out.strip().split("=")[1].replace(":", "")
+
+    def sign(self, data: bytes) -> bytes:
+        datafile = self._tmp(".bin")
+        sigfile = self._tmp(".p7s")
+        datafile.write_bytes(data)
+
+        subprocess.check_call([
+            "openssl", "cms", "-sign",
+            "-binary",
+            "-in", str(datafile),
+            "-signer", self.cert_ref.identifier,
+            "-inkey", self.cert_ref.private_key_path,     # operator-provided
+            "-outform", "DER",
+            "-out", str(sigfile)
+        ])
+
+        out = sigfile.read_bytes()
+        datafile.unlink()
+        sigfile.unlink()
+        return out
+
+    def verify(self, data: bytes, signature: bytes) -> bool:
+        datafile = self._tmp(".bin")
+        sigfile  = self._tmp(".p7s")
+        datafile.write_bytes(data)
+        sigfile.write_bytes(signature)
+
+        try:
+            subprocess.check_call([
+                "openssl", "cms", "-verify",
+                "-in", str(sigfile),
+                "-inform", "DER",
+                "-content", str(datafile),
+                "-CAfile", self.cert_ref.public_cert_path,
+                "-purpose", "any",
+                "-out", "/dev/null",
+            ])
+            return True
+        except subprocess.CalledProcessError:
+            return False
+        finally:
+            datafile.unlink()
+            sigfile.unlink()
+
+class WindowsCertutilBackend(SigningBackend):
+
+    @property
+    def certificate_thumbprint(self) -> str:
+        return self.cert_ref.identifier.upper().replace(":", "")
+
+    def sign(self, data: bytes) -> bytes:
+        datafile = self._tmp(".bin")
+        sigfile  = self._tmp(".p7s")
+        datafile.write_bytes(data)
+
+        cmd = [
+            "certutil",
+            "-user",
+            "-silent",
+            "-p", "",
+            "-sign",
+            str(datafile),
+            str(sigfile),
+            self.certificate_thumbprint
+        ]
+
+        subprocess.check_call(cmd)
+        out = sigfile.read_bytes()
+        datafile.unlink()
+        sigfile.unlink()
+        return out
+
+    def verify(self, data: bytes, signature: bytes) -> bool:
+        datafile = self._tmp(".bin")
+        sigfile = self._tmp(".p7s")
+        datafile.write_bytes(data)
+        sigfile.write_bytes(signature)
+
+        try:
+            subprocess.check_call([
+                "certutil", "-verify", str(sigfile), str(datafile)
+            ])
+            return True
+        except subprocess.CalledProcessError:
+            return False
+        finally:
+            datafile.unlink()
+            sigfile.unlink()
+
+class MacOSSecurityCMSBackend(SigningBackend):
+
+    @property
+    def certificate_thumbprint(self) -> str:
+        # Derive from provided public cert
+        out = subprocess.check_output([
+            "openssl", "x509", "-in", self.cert_ref.public_cert_path,
+            "-noout", "-fingerprint", "-sha1"
+        ]).decode()
+        return out.strip().split("=")[1].replace(":", "")
+
+    def sign(self, data: bytes) -> bytes:
+        datafile = self._tmp(".bin")
+        sigfile = self._tmp(".p7s")
+        datafile.write_bytes(data)
+
+        subprocess.check_call([
+            "security", "cms", "-S",
+            "-N", self.cert_ref.identifier,   # CN
+            "-i", str(datafile),
+            "-o", str(sigfile)
+        ])
+
+        out = sigfile.read_bytes()
+        datafile.unlink()
+        sigfile.unlink()
+        return out
+
+    def verify(self, data: bytes, signature: bytes) -> bool:
+        sigfile = self._tmp(".p7s")
+        datafile = self._tmp(".bin")
+        sigfile.write_bytes(signature)
+        datafile.write_bytes(data)
+
+        # explicit verify (preferred)
+        if self.cert_ref.public_cert_path:
+            try:
+                subprocess.check_call([
+                    "openssl", "cms", "-verify",
+                    "-in", str(sigfile),
+                    "-inform", "DER",
+                    "-content", str(datafile),
+                    "-CAfile", self.cert_ref.public_cert_path,
+                    "-purpose", "any",
+                    "-out", "/dev/null",
+                ])
+                return True
+            except subprocess.CalledProcessError:
+                return False
+
+        # fallback: structural validation only
+        try:
+            subprocess.check_call(["security", "cms", "-D", "-i", str(sigfile)])
+            return True
+        except subprocess.CalledProcessError:
+            return False
+        finally:
+            sigfile.unlink()
+            datafile.unlink()
+
+def get_signing_backend(cert_ref):
+    system = platform.system()
+    if system == "Linux":
+        return LinuxOpenSSLBackend(cert_ref)
+    if system == "Windows":
+        return WindowsCertutilBackend(cert_ref)
+    if system == "Darwin":
+        return MacOSSecurityCMSBackend(cert_ref)
+    raise RuntimeError("Unsupported platform")
Index: g/FlexoEntity.org
===================================================================
--- org/FlexoEntity.org	(revision 376e21b3865f5979339d98773fc106b2673d35a9)
+++ 	(revision )
@@ -1,225 +1,0 @@
-* Tech-Talk - Hashes / IDs / Lebenszyklen / FlexoEntity
-:PROPERTIES:
-:CUSTOM_ID: hashes-ids-lifecycle
-:END:
-
-** Was ist ein Hash?
-
-Ein Hash ist ein hoffentlich eindeutiger "Fingerabdruck" für Daten, der
-sich aus eben diesen Daten über den Algorithmus einer Hash-Funktion errechnet.
-
-Die Eingabedaten bezeichnet man als Seed, das Ergebnis als Digest.
-
-Man gibt die Saat (das Futter) an die Hashfunktion zum Zerkauen/Zerkleinern (Hashen),
-das wird verdaut, nebenbei noch gewürzt (Salt) und kommt als Digest wieder heraus.
-Die Länge der Ausgabe ist fest.
-
-** Ein wichtiger Punkt
-
-Wichtig ist auch zu verstehen, dass Hashes sogenannte "One-Way-Funktionen" sind.
-Das heißt, man kann das Hashing nicht einfach umkehren, um das ursprüngliche Objekt wiederherzustellen.
-Ein Hash ist also eine Einbahnstraße: Er dient nur dazu, die Daten zu repräsentieren,
-aber man kann nicht vom Hash auf die originalen Daten zurückrechnen.
-Aus Kacke kann man kein Essen zaubern.
-
-** Was ist kein Hash?
-
-Die ISBN identifiziert ein Buch eindeutig. Sie ist nicht das Buch selbst, sondern eher ein
-Identifier (eine geordnete Nummer mit Prüfziffer). Sie ist aber kein Hash, weil sie nicht
-aus dem Inhalt des Buches berechnet wird. Ähnliches gilt für die IBAN
-
-|-------------------------+---------------------------+-------------------------------------|
-| Merkmal                 | Hash                      | Identifier (z. B. ISBN, IBAN, UUID) |
-|-------------------------+---------------------------+-------------------------------------|
-| Abhängig vom Inhalt     | ja                        | nein                                |
-| Ziel                    | Integrität / Vergleich    | Identität / Referenz                |
-| Länge                   | fix z. B. 32 Hex-Zeichen  | frei definierbar                    |
-| Zufällig oder berechnet | deterministisch berechnet | oft generiert oder vergeben         |
-|-------------------------+---------------------------+-------------------------------------|
-
-** Warum verwenden wir Hashes?
-
-Hashes sind super praktisch, weil sie uns eine Menge Zeit und Rechenleistung sparen.
-Anstatt zwei große Dateien oder ganze Bücher direkt miteinander zu vergleichen,
-vergleichen wir einfach ihre Hashes. Wenn die Hashes gleich sind, wissen wir,
-dass die Daten sehr wahrscheinlich identisch sind. Wenn sie unterschiedlich sind,
-hat sich etwas geändert. Das spart enorm viel Aufwand.
-
-** Beispiele für den Einsatz von Hashes
-
-- **Passwort-Hashes**: Wenn du ein Passwort eingibst, wird oft nur der Hash gespeichert, nicht das
-   eigentliche Passwort. So bleibt es sicher, selbst wenn jemand die Datenbank stiehlt.
-
-   Beispiel aus der /etc/shadow-Datei
-
-root:$6$tsicHaoV3Q$YtAbiIvrHGXFtAJYz9tcEYWHXiGVQ40sJAgzPAbc57lIq9jH8eYjWXwctSW6YQnrMznRFcm6yXLnnY9mHhso20
-enno:$6$sdygzfEgx0$YpaZJMQdkZgxGPclphz6RojqNG.PSNEq1oIHRP4kvZRN2iuS5MQrxt0nCkrYQIcpDGyohrb1o0S/GkWrFriWL1
-
-Der Hash ist länger als das Passwort. Das dient der Sicherheit, weil man zwar nicht zurückrechnen,
-wohl aber vorwärtsrechnen kann. Hash-Funktionen sind deterministisch.
-Bei Linux-Passworteinträgen hat man hier $6$ den Hashalgorithmus kodiert, im daraufolgendem $.....$ das Salz
-und im Rest den eigentlichen Hash kodiert
-
-- **Git-Commits**: In Versionskontrollsystemen wie Git werden Hashes benutzt, um jeden Schwung an
-  Änderungen mit einer eindeutigen Kennung zu versehen
-
-- **URL-Shortener: hash("https://example.com") → 8a3f12
-  
-** FlexOID
-
-Die FlexOID ist eine Mischform. Sie selbst ist ein Identifier, der als Bestandteil aber einen Hash
-enthält, um möglichst eindeutig, aber nicht zu lang zu sein.
-
-AF-Q251022-70F759@001D
-│  │ │       │     │ │
-│  │ │       │     │ └── State (Draft, Approved, Signed, Published, Obsolete)
-│  │ │       │     └──── Version
-│  │ │       └────────── Hash (3 bytes, 6 Stellen)
-│  │ └────────────────── Date (YYMMDD)
-│  └──────────────────── Entity type (Question)
-└─────────────────────── Domain (e.g. Air force)
-
-In der ersten Variante der FlexOID habe ich mich mit einem 6-stelligen Hash zufrieden gegeben,
-weil das einfach lesbarer ist. Warum reicht das nicht?
-
-** Das Geburtstags-Paradoxon
-
-Der obige 3-Byte Hash liefert mir etwas über 16 Millionen unterschiedlich Varianten.
-Das klingt viel. Ist es aber nicht. Wieviele Schüler müssen nacheinander die Klasse
-betreten bis sich zwei finden, die mit mehr als 50 Prozent Wahrscheinlichkeit am gleichen
-Tag Geburtstag (also ein identisches Merkmal) haben?
-
-** Lösung
-
-Ab 23 Schülern beträgt die Wahrscheinlichkeit 50 % (näherungsweise Wurzel 365 Tagen)
-
-Bei 47 Schülern beträgt die Wahrscheinlichkeit bereits 95 Prozent
-
-Unsere 3 Bytes ergeben zwar 16.000.000 mögliche Varianten - im Gegenzug zu den 365 Tagen im Jahr
-aus dem Geburtstagsbeispiel - aber da die Wahrscheinlichkeit zur Kollision hier bei
-etwa Wurzel 16 Mio. liegt, bekommt man bereits bei 4000 Neuzugängen die ersten
-Übereinstimmungen im Hash.
-
-Der Hash ist also nicht gut genug, weil man sehr schnell und sehr häufig, diese
-Übereinstimmungen feststellen und behandeln müsste, wenn man weiterhin eindeutige
-Zuordnungen treffen will.
-
-Wenn man die Ausgabe der Hashfunktion auf 6 Bytes erweitert, kommen die ersten Kollisionen
-erst bei etwa 20 Mio erzeugten Hashes (Fingerabdrücken) und die kann man dann ohne
-Einbußen gesondert behandeln (Salzen), weil es so selten passiert.
-
-Übrigens, wenn man beim Menschen den Daumenabdruck nimmt und sich dabei auf 12 Merkmale
-beschränkt und sehr lockere Toleranzen ansetzt (was man in der Praxis nicht macht),
-hat man bereits nach 14000 Menschen eine Übereinstimmung. Bei 24 Merkmalen und sehr
-lockeren Toleranzen, hat man bei etwa nach 9 Mio. Menschen eine ungefähre Übereinstimmung.
-Da muss die Polizei schon sehr schlampig arbeiten, damit man fälschlicherweise beschuldigt wird.
-Die Zahlen in der Realität sind sogar noch deutlich höher.
-
-** FlexoEntity
-
-Nun haben wir gesehen, dass wir mit der FlexOID (mit 6-Byte Hash) sehr viele unterschiedliche
-Dinge eindeutig bestimmen können. Da unsere FlexOID erstmal nur eine Zeichenfolge ist,
-brauchen wir etwas das damit umgehen kann und was dafür verantwortlich ist. Das ist die FlexoEntity.
-
-Sie beinhaltet zusätzlich eine Signatur und ein Origin-Feld, wo festgehalten wird, woher diese
-Entität stammt (beispielsweise aus einer Hashkollision oder einer Änderung an den weiteren Daten)
-
-Jede Klasse, die von FlexoEntity erbt, muss zwingend die Method "text_seed" implementieren, mit der
-der Algorithmus einer Hash-Funktion gefüttert wird, aus der dann der 6-Byte Hash herauspurzelt.
-Hashfunktionen sind z.B. MD5, SHA1, SHA256 oder wie von mir genutzt Blake2s.
-Die Mathematik dahinter ist recht aufwändig, aber wer sich mal einlesen möchte
-
-- Hashing in Smalltalk: Theory and Practice von Andres Valloud
-
-Damit die Hash-Funktion genug Eingabedaten pro Entity hat, muss man sich überlegen, welche Merkmale
-einer Entität man durch "text_seed" übermittelt.
-
-** text_seed
-
-Man kann beliebige Klassen von FlexoEntity ableiten und erhält ohne Aufwand die Funktionalität
-zur eindeutigen Identifizierung und zur Lebenszyklus-Verwaltung
-
-
-Das ist das Beispiel einer OptionQuestion, also einer Testfrage, wo mögliche Antworten enthalten sind
-
-    @property
-    def text_seed(self) -> str:
-        """Include answer options (and points) for deterministic ID generation."""
-        base = super().text_seed
-        if not self.options:
-            return base
-
-        joined = "|".join(
-            f"{opt.text.strip()}:{opt.points}"
-            for opt in sorted(self.options, key=lambda o: o.text.strip().lower())
-        )
-        return f"{base}::{joined}"
-
-** Lebenszyklus
-
-Der Lebenszyklus einer Entität folgt dieser Reihenfolge und ist nicht umkehrbar
-
-- Entwurf (DRAFT)
-- Genehmigt (APPROVED)
-- Unterschrieben (SIGNED)
-- Veröffentlicht (PUBLISHED)
-- Veraltet (OBSOLETE)
-
-Eine Entität, die bereits die Stufe Veröffentlicht erreicht hat, kann nicht in die Stufe
-(nur) Unterschrieben zurückkehren. Daher ist auch die Lebenszyklusstufe in der ID kodiert
-(letztes Symbol der FlexOID)
-
-Beispiele für Entitäten:
-
-- Testfrage
-- Fragenkatalog
-- Einstufungstest
-- Zertifikat
-
-Ein veröffentlichter Einstufungstest kann nur Fragen beinhalten, die ihrerseits die Stufe Veröffentlicht
-erreicht haben. Ein Zertifikat kann nur ausgestellt werden, wenn der passende Einstufungstest die Stufe
-Veröffentlicht hat. Das origin-Feld des Zertifikats sollte sinnvollerweise die ID des Tests enthalten.
-
-** Erhöhung der Versionsnummer oder neue ID
-
-Sobald - aus Gründen - eine neue ID vergeben werden muss, wird ggf. die Ursprungs-ID
-im Feld origin der neuen FlexoEntity gespeichert. So ist immer ein Suchen im Stammbaum möglich.
-
-AF-Q251022-70F759@001D
-│  │ │       │     │ │
-│  │ │       │     │ └── State (Draft, Approved, Signed, Published, Obsolete)
-│  │ │       │     └──── Version
-│  │ │       └────────── Hash (3 bytes, 6 Stellen)
-│  │ └────────────────── Date (YYMMDD)
-│  └──────────────────── Entity type (Question)
-└─────────────────────── Domain (e.g. Air force)
-
-
-Eine einfache Erhöhung der Versionsnummer (am Ende der ID) ist unter Umständen auch ausreichend,
-damit nicht zu häufig komplett neue IDs erzeugt werden müssen. Der Entscheidungsbaum dazu ist
-für das Projekt FlexoGrader recht umfangreich und soll hier nicht weiter besprochen werden.
-Da es aber im Hintergrund passiert und vom Endanwender nicht bemerkt wird, behindert der
-Mechanismus die Nutzung des FlexoGrader nicht. 
-
-** Reifegrad
-
-Das Konzept und Design hat die Alpha-Phase verlassen, der Code ist aber noch deutlich Beta.
-RC1 etwa Anfang Dezember (nach meinem Urlaub). Änderungen an der FlexOID sind nicht mehr zu erwarten, beim API der FlexoEntity schon eher.
-
-Das Perfekte ist der Feind des Guten, aber ...
-
-Als Gegenbeispiel für unausgereiftes Design ist Pythons eingebaute datetime Bibliothek.
-Da könnte man ein Buch drüber schreiben. Halb Drama, halb Komödie.
-
-- dateutil, arrow, pendulum, maya, moment, delorean, pytz, zoneinfo + numpy/pandas
-  
-** Lizenz
-
-Da ich die FlexoEntity-Bibliothek in meiner Freizeit entworfen und programmiert habe,
-bin ich alleiniger Urheber. Als Lizenz habe ich MIT gewählt, somit kann - unter Nennung
-des Urhebers - jeder damit machen, was er möchte
-
-Der FlexoGrader ist ein Bundeswehrprojekt, welches ich im Dienst programmiere und somit
-gehen die Nutzungsrechte uneingeschränkt auf den Dienstherren über.
-
-Das betrifft auch etwaige Folgeprojekte wie FlexoVault und FlexoDrill
Index: org/FlexoEntityTalk.org
===================================================================
--- org/FlexoEntityTalk.org	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
+++ org/FlexoEntityTalk.org	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -0,0 +1,224 @@
+* Tech-Talk - Hashes / IDs / Lebenszyklen / FlexoEntity
+:PROPERTIES:
+:CUSTOM_ID: hashes-ids-lifecycle
+:END:
+
+** Was ist ein Hash?
+
+Ein Hash ist ein hoffentlich eindeutiger "Fingerabdruck" für Daten, der
+sich aus eben diesen Daten über den Algorithmus einer Hash-Funktion errechnet.
+
+Die Eingabedaten bezeichnet man als Seed, das Ergebnis als Digest.
+
+Man gibt die Saat (das Futter) an die Hashfunktion zum Zerkauen/Zerkleinern (Hashen),
+das wird verdaut, nebenbei noch gewürzt (Salt) und kommt als Digest wieder heraus.
+Die Länge der Ausgabe ist fest.
+
+** Ein wichtiger Punkt
+
+Wichtig ist auch zu verstehen, dass Hashes sogenannte "One-Way-Funktionen" sind.
+Das heißt, man kann das Hashing nicht einfach umkehren, um das ursprüngliche Objekt wiederherzustellen.
+Ein Hash ist also eine Einbahnstraße: Er dient nur dazu, die Daten zu repräsentieren,
+aber man kann nicht vom Hash auf die originalen Daten zurückrechnen.
+Aus Kacke kann man kein Essen zaubern.
+
+** Was ist kein Hash?
+
+Die ISBN identifiziert ein Buch eindeutig. Sie ist nicht das Buch selbst, sondern eher ein
+Identifier (eine geordnete Nummer mit Prüfziffer). Sie ist aber kein Hash, weil sie nicht
+aus dem Inhalt des Buches berechnet wird. Ähnliches gilt für die IBAN
+
+|-------------------------+---------------------------+-------------------------------------|
+| Merkmal                 | Hash                      | Identifier (z. B. ISBN, IBAN, UUID) |
+|-------------------------+---------------------------+-------------------------------------|
+| Abhängig vom Inhalt     | ja                        | nein                                |
+| Ziel                    | Integrität / Vergleich    | Identität / Referenz                |
+| Länge                   | fix z. B. 32 Hex-Zeichen  | frei definierbar                    |
+| Zufällig oder berechnet | deterministisch berechnet | oft generiert oder vergeben         |
+|-------------------------+---------------------------+-------------------------------------|
+
+** Warum verwenden wir Hashes?
+
+Hashes sind super praktisch, weil sie uns eine Menge Zeit und Rechenleistung sparen.
+Anstatt zwei große Dateien oder ganze Bücher direkt miteinander zu vergleichen,
+vergleichen wir einfach ihre Hashes. Wenn die Hashes gleich sind, wissen wir,
+dass die Daten sehr wahrscheinlich identisch sind. Wenn sie unterschiedlich sind,
+hat sich etwas geändert. Das spart enorm viel Aufwand.
+
+** Beispiele für den Einsatz von Hashes
+
+- **Passwort-Hashes**: Wenn du ein Passwort eingibst, wird oft nur der Hash gespeichert, nicht das
+   eigentliche Passwort. So bleibt es sicher, selbst wenn jemand die Datenbank stiehlt.
+
+   Beispiel aus der /etc/shadow-Datei
+
+root:$6$tsicHaoV3Q$YtAbiIvrHGXFtAJYz9tcEYWHXiGVQ40sJAgzPAbc57lIq9jH8eYjWXwctSW6YQnrMznRFcm6yXLnnY9mHhso20
+enno:$6$sdygzfEgx0$YpaZJMQdkZgxGPclphz6RojqNG.PSNEq1oIHRP4kvZRN2iuS5MQrxt0nCkrYQIcpDGyohrb1o0S/GkWrFriWL1
+
+Der Hash ist länger als das Passwort. Das dient der Sicherheit, weil man zwar nicht zurückrechnen,
+wohl aber vorwärtsrechnen kann. Hash-Funktionen sind deterministisch.
+Bei Linux-Passworteinträgen hat man hier $6$ den Hashalgorithmus kodiert, im daraufolgendem $.....$ das Salz
+und im Rest den eigentlichen Hash kodiert
+
+- **Git-Commits**: In Versionskontrollsystemen wie Git werden Hashes benutzt, um jeden Schwung an
+  Änderungen mit einer eindeutigen Kennung zu versehen
+
+- **URL-Shortener: hash("https://example.com") → 8a3f12
+  
+** FlexOID
+
+Die FlexOID ist eine Mischform. Sie selbst ist ein Identifier, der als Bestandteil aber einen Hash
+enthält, um möglichst eindeutig, aber nicht zu lang zu sein.
+
+AF-I251022-70F759@001D
+│  │ │       │     │ │
+│  │ │       │     │ └── State (Draft, Approved, Signed, Published, Obsolete)
+│  │ │       │     └──── Version
+│  │ │       └────────── Hash (6 bytes, 12 Stellen)
+│  │ └────────────────── Date (YYMMDD)
+│  └──────────────────── Entity type (ITEM)
+└─────────────────────── Domain (e.g. Air force)
+
+In der ersten Variante der FlexOID habe ich mich mit einem 6-stelligen Hash zufrieden gegeben,
+weil das einfach lesbarer ist. Warum reicht das nicht?
+
+** Das Geburtstags-Paradoxon
+
+Der obige 3-Byte Hash liefert mir etwas über 16 Millionen unterschiedlich Varianten.
+Das klingt viel. Ist es aber nicht. Wieviele Schüler müssen nacheinander die Klasse
+betreten bis sich zwei finden, die mit mehr als 50 Prozent Wahrscheinlichkeit am gleichen
+Tag Geburtstag (also ein identisches Merkmal) haben?
+
+** Lösung
+
+Ab 23 Schülern beträgt die Wahrscheinlichkeit 50 % (näherungsweise Wurzel 365 Tagen)
+
+Bei 47 Schülern beträgt die Wahrscheinlichkeit bereits 95 Prozent
+
+Unsere 3 Bytes ergeben zwar 16.000.000 mögliche Varianten - im Gegenzug zu den 365 Tagen im Jahr
+aus dem Geburtstagsbeispiel - aber da die Wahrscheinlichkeit zur Kollision hier bei
+etwa Wurzel 16 Mio. liegt, bekommt man bereits bei 4000 Neuzugängen die ersten
+Übereinstimmungen im Hash.
+
+Der Hash ist also nicht gut genug, weil man sehr schnell und sehr häufig, diese
+Übereinstimmungen feststellen und behandeln müsste, wenn man weiterhin eindeutige
+Zuordnungen treffen will.
+
+Wenn man die Ausgabe der Hashfunktion auf 6 Bytes erweitert, kommen die ersten Kollisionen
+erst bei etwa 20 Mio erzeugten Hashes (Fingerabdrücken) und die kann man dann ohne
+Einbußen gesondert behandeln (Salzen), weil es so selten passiert.
+
+Übrigens, wenn man beim Menschen den Daumenabdruck nimmt und sich dabei auf 12 Merkmale
+beschränkt und sehr lockere Toleranzen ansetzt (was man in der Praxis nicht macht),
+hat man bereits nach 14000 Menschen eine Übereinstimmung. Bei 24 Merkmalen und sehr
+lockeren Toleranzen, hat man bei etwa nach 9 Mio. Menschen eine ungefähre Übereinstimmung.
+Da muss die Polizei schon sehr schlampig arbeiten, damit man fälschlicherweise beschuldigt wird.
+Die Zahlen in der Realität sind sogar noch deutlich höher.
+
+** FlexoEntity
+
+Nun haben wir gesehen, dass wir mit der FlexOID (mit 6-Byte Hash) sehr viele unterschiedliche
+Dinge eindeutig bestimmen können. Da unsere FlexOID erstmal nur eine Zeichenfolge ist,
+brauchen wir etwas das damit umgehen kann und was dafür verantwortlich ist. Das ist die FlexoEntity.
+
+Sie beinhaltet zusätzlich ein Origin-Feld, wo festgehalten wird, woher diese
+Entität stammt (beispielsweise aus einer Hashkollision oder einer Änderung an den weiteren Daten)
+
+Jede Klasse, die von FlexoEntity erbt, muss zwingend die Methode "text_seed" implementieren,
+mit der der Algorithmus einer Hash-Funktion gefüttert wird, aus der dann der 6-Byte Hash herauspurzelt.
+Hashfunktionen sind z.B. MD5, SHA1, SHA256 oder wie von mir genutzt: Blake2s.
+Die Mathematik dahinter ist recht aufwändig, aber wer sich mal einlesen möchte
+
+- Hashing in Smalltalk: Theory and Practice von Andres Valloud
+
+Damit die Hash-Funktion genug Eingabedaten pro Entity hat, muss man sich überlegen, welche Merkmale
+einer Entität man durch "text_seed" übermittelt.
+
+** text_seed
+
+Man kann beliebige Klassen von FlexoEntity ableiten und erhält ohne Aufwand die Funktionalität
+zur eindeutigen Identifizierung und zur Lebenszyklus-Verwaltung
+
+Das ist das Beispiel einer ChoiceQuestion, also einer Testfrage, wo mögliche Antworten enthalten sind
+
+    @property
+    def text_seed(self) -> str:
+        """Include answer options (and points) for deterministic ID generation."""
+        base = super().text_seed
+        if not self.options:
+            return base
+
+        joined = "|".join(
+            f"{opt.text.strip()}:{opt.points}"
+            for opt in sorted(self.options, key=lambda o: o.text.strip().lower())
+        )
+        return f"{base}::{joined}"
+
+** Lebenszyklus
+
+Der Lebenszyklus einer Entität folgt dieser Reihenfolge und ist nicht umkehrbar
+
+- Entwurf (DRAFT)
+- Genehmigt (APPROVED)
+- Unterschrieben (APPROVED_AND_SIGNED)
+- Veröffentlicht (PUBLISHED)
+- Veraltet (OBSOLETE)
+
+Eine Entität, die bereits die Stufe Veröffentlicht erreicht hat, kann nicht in die Stufe
+(nur) Unterschrieben zurückkehren. Daher ist auch die Lebenszyklusstufe in der ID kodiert
+(letztes Symbol der FlexOID)
+
+Beispiele für Entitäten:
+
+- Testfrage
+- Fragenkatalog
+- Einstufungstest
+- Zertifikat
+
+Ein veröffentlichter Einstufungstest kann nur Fragen beinhalten, die ihrerseits die Stufe Veröffentlicht
+erreicht haben. Ein Zertifikat kann nur ausgestellt werden, wenn der passende Einstufungstest die Stufe
+Veröffentlicht hat. Das origin-Feld des Zertifikats sollte sinnvollerweise die ID des Tests enthalten.
+
+** Erhöhung der Versionsnummer oder neue ID
+
+Sobald - aus Gründen - eine neue ID vergeben werden muss, wird ggf. die Ursprungs-ID
+im Feld origin der neuen FlexoEntity gespeichert. So ist immer ein Suchen im Stammbaum möglich.
+
+AF-Q251022-70F759@001D
+│  │ │       │     │ │
+│  │ │       │     │ └── State (Draft, Approved, Signed, Published, Obsolete)
+│  │ │       │     └──── Version
+│  │ │       └────────── Hash (3 bytes, 6 Stellen)
+│  │ └────────────────── Date (YYMMDD)
+│  └──────────────────── Entity type (Question)
+└─────────────────────── Domain (e.g. Air force)
+
+
+Eine einfache Erhöhung der Versionsnummer (am Ende der ID) ist unter Umständen auch ausreichend,
+damit nicht zu häufig komplett neue IDs erzeugt werden müssen. Der Entscheidungsbaum dazu ist
+für das Projekt FlexoGrader recht umfangreich und soll hier nicht weiter besprochen werden.
+Da es aber im Hintergrund passiert und vom Endanwender nicht bemerkt wird, behindert der
+Mechanismus die Nutzung des FlexoGrader nicht. 
+
+** Reifegrad
+
+Das Konzept und Design hat die Alpha-Phase verlassen, der Code ist aber noch deutlich Beta.
+RC1 etwa Anfang Dezember (nach meinem Urlaub). Änderungen an der FlexOID sind nicht mehr zu erwarten, beim API der FlexoEntity schon eher.
+
+Das Perfekte ist der Feind des Guten, aber ...
+
+Als Gegenbeispiel für unausgereiftes Design ist Pythons eingebaute datetime Bibliothek.
+Da könnte man ein Buch drüber schreiben. Halb Drama, halb Komödie.
+
+- dateutil, arrow, pendulum, maya, moment, delorean, pytz, zoneinfo + numpy/pandas
+  
+** Lizenz
+
+Da ich die FlexoEntity-Bibliothek in meiner Freizeit entworfen und programmiert habe,
+bin ich alleiniger Urheber. Als Lizenz habe ich MIT gewählt, somit kann - unter Nennung
+des Urhebers - jeder damit machen, was er möchte
+
+Der FlexoGrader ist ein Bundeswehrprojekt, welches ich im Dienst programmiere und somit
+gehen die Nutzungsrechte uneingeschränkt auf den Dienstherren über.
+
+Das betrifft auch etwaige Folgeprojekte wie FlexoVault und FlexoDrill
Index: org/README.org
===================================================================
--- org/README.org	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
+++ org/README.org	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -0,0 +1,291 @@
+#+TITLE: flexoentity
+#+SUBTITLE: A hardened entity base and deterministic identifier system for the Flex-O family
+#+AUTHOR: Enno
+#+DATE: 2025-10-20
+#+OPTIONS: toc:3 num:nil
+
+* Overview
+
+`flexoentity` provides the *identity and lifecycle backbone* for all Flex-O components  
+(Flex-O-Grader, Flex-O-Vault, Flex-O-Drill, …).
+
+It defines how entities such as questions, media, catalogs, and exams are *identified, versioned, signed, and verified* — all without any external database dependencies.
+
+At its heart lie two modules:
+
+- =id_factory.py= – robust, cryptographically-verifiable *Flex-O ID generator*
+- =flexo_entity.py= – abstract *base class for all versioned entities*
+
+Together, they form a compact yet powerful foundation for audit-ready, reproducible data structures across offline and air-gapped deployments.
+
+- Design Goals
+
+|----------------|--------------------------------------------------------------------------------------------------------|
+| Goal           | Description                                                                                            |
+|----------------|--------------------------------------------------------------------------------------------------------|
+| *Determinism*  | IDs are derived from canonicalized entity content — identical input always yields identical ID prefix. |
+| *Integrity*    | BLAKE2s hashing and digital signatures protect against manual tampering.                               |
+| *Traceability* | Version numbers (=@001A=, =@002S=, …) track entity lifecycle transitions.                              |
+| *Stability*    | Hash prefixes remain constant across state changes; only version and state suffixes evolve.            |
+| *Auditability* | Every entity can be serialized, verified, and reconstructed without hidden dependencies.               |
+| *Simplicity*   | Pure-Python, zero external libraries, self-contained and easy to embed.                                |
+|----------------|--------------------------------------------------------------------------------------------------------|
+
+* Flex-O ID Structure
+
+Each entity carries a unique *Flex-O ID*, generated by =FlexOID.generate()=.
+
+#+BEGIN_EXAMPLE
+AF-I250101-9A4C2D@003S
+#+END_EXAMPLE
+
+|-----------+-----------------+----------------------------------------------|
+| Segment   | Example         | Meaning                                      |
+|-----------+-----------------+----------------------------------------------|
+| *Domain*  | =AF or PY_LANG= | Uppercase - Logical scope (e.g. "Air Force") |
+| *Type*    | =I=             | Entity type (e.g. ITEM)                      |
+| *Date*    | =250101=        | UTC creation date (YYMMDD)                   |
+| *Hash*    | =9A4C2D4F6E53=  | 12-digit BLAKE2s digest of canonical content |
+| *Version* | =003=           | Sequential version counter                   |
+| *State*   | =S=             | Lifecycle state (D, A, S, P, O)              |
+|-----------+-----------------+----------------------------------------------|
+
+
+* Lifecycle States
+
+|-----------------------|---------|-----------------------------|
+| State                 | Abbrev. | Description                 |
+|-----------------------|---------|-----------------------------|
+| *DRAFT*               | =D=     | Editable, not yet validated |
+| *APPROVED*            | =A=     | Reviewed and accepted       |
+| *APPROVED_AND_SIGNED* | =S=     | Cryptographically signed    |
+| *PUBLISHED*           | =P=     | Released to consumers       |
+| *OBSOLETE*            | =O=     | Archived or replaced        |
+|-----------------------|---------|-----------------------------|
+
+Transitions follow a strict progression:
+#+BEGIN_EXAMPLE
+DRAFT -> APPROVED -> APPROVED_AND_SIGNED -> PUBLISHED -> OBSOLETE
+#+END_EXAMPLE
+
+Only DRAFT entities can be deleted - all others got OBSOLETE mark instead
+
+* Core Classes
+
+** FlexOID
+
+A lightweight immutable class representing the full identity of an entity.
+
+*Highlights*
+- safe_generate(domain, entity_type, estate, text, version=1, repo) -> create a new ID
+- next_version(oid) -> increment version safely
+- clone_new_base(domain, entity_type, estate, text) -> start a new lineage
+- Deterministic prefix, state-dependent signature
+
+** =FlexoEntity=
+Abstract base class for all versioned entities (e.g., Question, Exam, Catalog).
+
+Implements:
+- ID lifecycle management (approve(), sign(), publish(), obsolete())
+- Serialization (to_json(), from_json(), to_dict(), from_dict())
+- Integrity verification (verify_integrity(entity))
+- Controlled state transitions with automatic timestamps
+
+Subclasses define a single property:
+
+#+BEGIN_SRC python
+@property
+def text_seed(self) -> str:
+    """Canonical text or core content for hashing."""
+#+END_SRC
+
+* Integrity Verification
+
+Each entity can self-verify its integrity:
+
+#+BEGIN_SRC python
+entity = Question.with_domain_id(domain_id="AF", text="What is Ohm’s law?", topic="Electronics")
+json_str = entity.to_json()
+reloaded = Question.from_json(json_str)
+
+assert FlexoEntity.verify_integrity(reloaded)
+#+END_SRC
+
+If the file is tampered with (e.g. "Ohm’s" → "Omm’s"), verification fails:
+
+* Real World Example
+
+Below you can see the implementation of a dedicated FlexoEntity class, used for Domains.
+We set an ENTITY_TYPE and define the needed fields in the data class. We define how to create
+a default object, the text_seed (it is easy because the domain id is unique and therefore sufficient)
+and the methods for serialization.
+
+#+BEGIN_SRC python
+from uuid import UUID
+from dataclasses import dataclass
+from flexoentity import FlexOID, FlexoEntity, EntityType
+
+@dataclass
+class Domain(FlexoEntity):
+    """
+    I am a helper class to provide more information than just a
+    domain abbreviation in FlexOID, doing mapping and management
+    """
+
+    ENTITY_TYPE = EntityType.DOMAIN
+
+    fullname: str = ""
+    description: str = ""
+    classification: str = "UNCLASSIFIED"
+
+    @classmethod
+    def default(cls):
+        """Return the default domain object."""
+        return cls.with_domain_id(domain_id="GEN_GENERIC",
+                                  fullname="Generic Domain", classification="UNCLASSIFIED")
+
+    @property
+    def text_seed(self) -> str:
+        return self.domain_id
+
+    def to_dict(self):
+        base = super().to_dict()
+        base.update({
+            "flexo_id": self.flexo_id,
+            "domain_id": self.domain_id,
+            "fullname": self.fullname,
+            "description": self.description,
+            "classification": self.classification,
+        })
+        return base
+
+    @classmethod
+    def from_dict(cls, data):
+        # Must have flexo_id
+        if "flexo_id" not in data:
+            raise ValueError("Domain serialization missing 'flexo_id'.")
+
+        flexo_id = FlexOID(data["flexo_id"])
+
+        obj = cls(
+            fullname=data.get("fullname", ""),
+            description=data.get("description", ""),
+            classification=data.get("classification", "UNCLASSIFIED"),
+            flexo_id=flexo_id,
+            _in_factory=True
+        )
+
+        # Restore metadata
+        obj.origin = data.get("origin")
+        obj.fingerprint = data.get("fingerprint", "")
+        obj.originator_id = (
+            UUID(data["originator_id"]) if data.get("originator_id") else None
+        )
+        obj.owner_id = (
+            UUID(data["owner_id"]) if data.get("owner_id") else None
+        )
+
+        return obj
+#+END_SRC
+        
+* Usage
+#+BEGIN_SRC python
+d = Domain.default()
+print(d.flexo_id)             # GEN_GENERIC-D251124-67C2CAE292CE@001D
+d.approve()
+print(d.flexo_id)             # GEN_GENERIC-D251124-67C2CAE292CE@001A
+d.sign()
+print(d.flexo_id)             # GEN_GENERIC-D251124-67C2CAE292CE@001S
+#+END_SRC
+
+* Serialization Example
+
+#+BEGIN_SRC python 
+{
+    'flexo_id': FlexOID(GEN_GENERIC-D251124-29CE0F4BE59D@001S),
+    'fingerprint': '534BD2EC5C5511F1',
+    'origin': FlexOID(GEN_GENERIC-D251124-67C2CAE292CE@001D),
+    'originator_id': '00000000-0000-0000-0000-000000000000',
+    'owner_id': '00000000-0000-0000-0000-000000000000',
+    'domain_id': 'GEN_GENERIC',
+    'fullname': 'Generic Domain',
+    'description': '',
+    'classification': 'UNCLASSIFIED'}
+#+END_SRC
+
+#+BEGIN_SRC js
+
+{
+        "flexo_id": "GEN_GENERIC-D251124-29CE0F4BE59D@001S",
+        "fingerprint": "534BD2EC5C5511F1",
+        "origin": "GEN_GENERIC-D251124-67C2CAE292CE@001D",
+        "originator_id": "00000000-0000-0000-0000-000000000000",
+        "owner_id": "00000000-0000-0000-0000-000000000000",
+        "domain_id": "GEN_GENERIC",
+        "fullname": "Generic Domain",
+        "description": "",
+        "classification": "UNCLASSIFIED"
+}
+
+
+
+* Entity Type and State Codes
+
+|-------------+------+----------------------------------------------------------------------------|
+| EntityType  | Code | Typical Use                                                                |
+|-------------+------+----------------------------------------------------------------------------|
+| GENERIC     | G    | Generic entities that does not fit other types yet or are temporarily only |
+| DOMAIN      | D    | Every Domain is of this type                                               |
+| MEDIA       | M    | Every media item belongs to this type, e.g. Pictures, Audio, Video         |
+| ITEM        | I    | An Entity what is usually used in a collection, e.g. Questions in a test   |
+| COLLECTION  | C    | A collection of items, as an Exam or a catalog                             |
+| TEXT        | T    | A text document                                                            |
+| HANDOUT     | H    | A published document                                                       |
+| OUTPUT      | O    | The output of a computation                                                |
+| RECORD      | R    | Record type data, as bibliography entries                                  |
+| SESSION     | S    | A unique session, e.g. managed by a session manager
+| USER        | U    | User objects                                                               |
+| CONFIG      | F    | CONFIG files that need to be tracked over time and state                   |
+| EVENT       | E    | Events that have to be tracked over time, as status messages or orders     |
+| ATTESTATION | X    | Entities that attest a formal technical (not human) check e.g. Signatures  |
+|-------------+------+----------------------------------------------------------------------------|
+
+|---------------------|------|-------------------|
+| EntityState         | Code | Meaning           |
+|---------------------|------|-------------------|
+| DRAFT               | D    | Work in progress  |
+| APPROVED            | A    | Reviewed          |
+| APPROVED_AND_SIGNED | S    | Signed version    |
+| PUBLISHED           | P    | Publicly released |
+| OBSOLETE            | O    | Deprecated        |
+|---------------------|------|-------------------|
+
+* Design Notes
+- *Hash Stability:* Only domain, entity type, and content text influence the hash.
+  This ensures consistent prefixes across state changes.
+- *State-Dependent Signatures:* Each lifecycle stage has its own signature seed.
+  Modifying a file without re-signing invalidates integrity.
+- *Obsolescence Threshold:* Version numbers above 900 trigger warnings;
+  beyond 999 are considered obsolete.
+- *Clone Lineages:* Cloning an entity resets versioning but preserves metadata lineage.
+
+* Dependencies
+- Python 3.11+
+- Standard library only (=hashlib=, =json=, =datetime=, =enum=, =dataclasses=)
+
+No external packages. Fully compatible with *Guix*, *air-gapped* deployments, and *reproducible builds*.
+
+* Integration
+`flexoentity` is imported by higher-level modules such as:
+
+- *Flex-O-Grader* → manages question catalogs and exam bundles
+- *Flex-O-Vault* → provides persistent media storage with metadata integrity
+- *Flex-O-Drill* → uses versioned entities for training simulations
+
+All share the same identity and versioning logic — ensuring that
+*what was approved, signed, and published remains provably authentic.*
+
+* License
+MIT License 2025
+Part of the *Flex-O family* by Flex-O-Dyne GmbH 
+Designed for reproducible, audit-ready, human-centered software.
Index: org/certificates.org
===================================================================
--- org/certificates.org	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
+++ org/certificates.org	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -0,0 +1,210 @@
+* Flex-O Cross-Platform Signing System
+
+This document defines how Flex-O performs cryptographic signing across Linux, Windows, and macOS using a single interoperable workflow.
+
+** Requirements
+
+- PKCS#7 / CMS signatures
+- RSA-4096
+- DER output format (Windows/macOS native)
+- No Python crypto libraries
+- OS-native signing tooling
+
+* 1. Key and Certificate Creation (Universal)
+
+** 1.1 Generate a private key (recommended: RSA-4096)
+
+#+begin_src bash
+openssl genrsa -out mykey.pem 4096
+#+end_src
+
+** 1.2 Generate an X.509 certificate
+
+#+begin_src bash
+openssl req -new -x509 -key mykey.pem -out mycert.pem -days 3650 -sha256
+#+end_src
+
+Result:
+- mykey.pem → private key
+- mycert.pem → certificate (public key)
+
+* 2. Import Certificates into Each Platform
+
+** 2.1 Linux
+
+No import required. Use PEM files directly.
+
+** 2.2 Windows (PKCS#12 required)
+
+Create PKCS#12 bundle:
+
+#+begin_src bash
+openssl pkcs12 -export -out flexo.pfx -inkey mykey.pem -in mycert.pem
+#+end_src
+
+Import into user certificate store:
+
+#+begin_src bash
+certutil -user -p PASSWORD -importpfx flexo.pfx
+#+end_src
+
+** 2.3 macOS (PKCS#12)
+
+#+begin_src bash
+openssl pkcs12 -export -out flexo.p12 -inkey mykey.pem -in mycert.pem
+#+end_src
+
+Import:
+
+#+begin_src bash
+security import flexo.p12 -k ~/Library/Keychains/login.keychain-db
+#+end_src
+
+
+* 3. Cross-Platform Signing Commands
+
+** 3.1 Linux (OpenSSL CMS / DER)
+
+#+begin_src bash
+openssl cms -sign \
+    -binary \
+    -in data.txt \
+    -signer mycert.pem \
+    -inkey mykey.pem \
+    -outform DER \
+    -out signature.p7s
+#+end_src
+
+** 3.2 Windows (certutil)
+
+#+begin_src bash
+certutil -sign data.txt signature.p7s
+#+end_src
+
+** 3.3 macOS (`security cms`)
+
+#+begin_src bash
+security cms -S \
+    -N "Common Name of Cert" \
+    -i data.txt \
+    -o signature.p7s
+#+end_src
+
+All three platforms produce binary DER PKCS#7 signatures.
+
+* 4. Cross-Platform Verification
+
+** 4.1 Linux (OpenSSL)
+
+#+begin_src bash
+openssl cms -verify \
+    -in signature.p7s \
+    -inform DER \
+    -content data.txt \
+    -CAfile mycert.pem \
+    -purpose any \
+    -out /dev/null
+#+end_src
+
+** 4.2 Windows
+
+#+begin_src bash
+certutil -verify signature.p7s data.txt
+#+end_src
+
+** 4.3 macOS
+
+#+begin_src bash
+security cms -D -i signature.p7s > verified.txt
+#+end_src
+
+* 5. Flex-O Signing Specification
+
+** 5.1 Key Requirements
+- RSA-4096
+- X.509 certificate
+- Valid for ≥ 10 years
+
+** 5.2 Signature Format Requirements
+- PKCS#7/CMS
+- Binary DER form
+- Signing certificate must be embedded
+
+** 5.3 Verification Requirements
+- Must work with OpenSSL CMS
+- No dependency on OS certificate stores
+- Must accept DER PKCS#7 signatures
+
+** 5.4 Flex-O Signature Entity Schema
+Required fields:
+- signed_entity: FlexOID
+- signer_id: UUID
+- signature_data: base64(PKCS7 DER blob)
+- signature_type: "PKCS7-DER"
+- certificate_thumbprint: SHA-1 thumbprint
+- comment: optional
+
+** 5.5 Security Assumptions
+- Flex-O never stores private keys
+- OS handles private key protection
+- Only public certificates embedded in signatures
+
+* 6. Refer to certificates 
+
+** Linux
+
+#+BEGIN_SRC python
+cert_ref = CertificateReference(
+    platform="LINUX",
+    identifier="/etc/flexo/certs/mycert.pem",
+    private_key_path="$HOME/.flexo/mykey.pem",
+    public_cert_path="/etc/flexo/certs/mycert.pem",
+)
+backend = create_backend(cert_ref)
+signature_bytes = backend.sign(b"hello world")
+#+END_SRC
+
+
+** Windows
+
+#+BEGIN_SRC python
+cert_ref = CertificateReference(
+    platform="WINDOWS",
+    identifier="E1A2B3C4D5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B",   # Thumbprint
+)
+
+backend = create_backend(cert_ref)
+signature_bytes = backend.sign(b"hello world")
+#+END_SRC
+
+** MacOS
+
+#+BEGIN_SRC python
+cert_ref = CertificateReference(
+    platform="MACOS",
+    identifier="FlexOSigner", # Common Name (CN)
+    public_cert_path="/Users/enno/certs/FlexOSigner.pem"
+)
+
+backend = create_backend(cert_ref)
+signature_bytes = backend.sign(b"hello world")
+#+END_SRC
+
+* 8. Flex-O Signature Entity
+
+#+begin_src python
+@dataclass
+class Signature(FlexoEntity):
+    ENTITY_TYPE = EntityType.OUTPUT
+
+    signed_entity: Optional[FlexOID] = None
+    signer_id: Optional[UUID] = None
+    signature_data: str = ""          # Base64 of PKCS#7 DER
+    signature_type: str = "PKCS7-DER"
+    certificate_thumbprint: str = ""
+    comment: Optional[str] = None
+
+    @property
+    def text_seed(self) -> str:
+        return f"{self.signed_entity}:{self.signer_id}:{self.certificate_thumbprint}"
+#+end_src
Index: tests/conftest.py
===================================================================
--- tests/conftest.py	(revision 376e21b3865f5979339d98773fc106b2673d35a9)
+++ tests/conftest.py	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -1,8 +1,11 @@
 # tests/stubs/single_choice_question.py
+from dataclasses import dataclass, field
 import pytest
+import platform
+from pathlib import Path
 from datetime import datetime
-from dataclasses import dataclass, field
 from typing import List
-from flexoentity import FlexOID, FlexoEntity, EntityType, EntityState, Domain
+from flexoentity import FlexOID, FlexoEntity, EntityType, EntityState, Domain, get_signing_backend, CertificateReference
+
 
 @pytest.fixture
@@ -98,2 +101,84 @@
     q._update_fingerprint()
     return q
+
+SYSTEM = platform.system()
+
+
+# ─────────────────────────────────────────────────────────────
+# Basic test data directory + PEM test files
+# ─────────────────────────────────────────────────────────────
+
+@pytest.fixture(scope="session")
+def test_data_dir():
+    return Path(__file__).parent / "data"
+
+
+@pytest.fixture(scope="session")
+def test_cert(test_data_dir):
+    return test_data_dir / "testcert.pem"
+
+
+@pytest.fixture(scope="session")
+def test_key(test_data_dir):
+    return test_data_dir / "testkey.pem"
+
+
+# ─────────────────────────────────────────────────────────────
+# CertificateReference fixtures for each platform
+# ─────────────────────────────────────────────────────────────
+
+@pytest.fixture(scope="session")
+def cert_ref_linux(test_cert, test_key):
+    """Linux: Uses OpenSSL CMS with PEM cert + PEM private key."""
+    return CertificateReference(
+        platform="LINUX",
+        identifier=str(test_cert),
+        private_key_path=str(test_key),
+        public_cert_path=str(test_cert),
+    )
+
+
+@pytest.fixture(scope="session")
+def cert_ref_macos(test_cert):
+    """
+    macOS: Uses Keychain identity with Common Name (CN).
+    The test cert must be imported into the login keychain with CN=FlexOSignerTest.
+    """
+    return CertificateReference(
+        platform="MACOS",
+        identifier="FlexOSignerTest",
+        public_cert_path=str(test_cert),
+    )
+
+@pytest.fixture(scope="session")
+def backend(test_cert, test_key):
+    """Return the correct backend for the current platform."""
+
+    if SYSTEM == "Linux":
+        cert_ref = CertificateReference(
+            platform="LINUX",
+            identifier=str(test_cert),
+            private_key_path=str(test_key),
+            public_cert_path=str(test_cert),
+        )
+
+    elif SYSTEM == "Darwin":
+        cert_ref = CertificateReference(
+            platform="MACOS",
+            identifier="FlexOSignerTest",
+            public_cert_path=str(test_cert),
+        )
+
+    elif SYSTEM == "Windows":
+        pytest.skip("Windows signing tests not implemented yet")
+
+    else:
+        pytest.skip(f"Unsupported platform: {SYSTEM}")
+
+    try:
+        backend = get_signing_backend(cert_ref)
+        # sanity check: ensures cert exists and command is available
+        _ = backend.certificate_thumbprint
+        return backend
+    except Exception as e:
+        pytest.skip(f"Backend unavailable or misconfigured: {e}")
Index: tests/data/testcert.pem
===================================================================
--- tests/data/testcert.pem	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
+++ tests/data/testcert.pem	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFazCCA1OgAwIBAgIUChNYApno8zSGa2I1CAigI6Wm5JIwDQYJKoZIhvcNAQEL
+BQAwRTEYMBYGA1UEAwwPRmxleE9TaWduZXJUZXN0MQ4wDAYDVQQKDAVGbGV4TzEM
+MAoGA1UECwwDRGV2MQswCQYDVQQGEwJERTAeFw0yNTExMjQxMzIwMDhaFw0zNTEx
+MjIxMzIwMDhaMEUxGDAWBgNVBAMMD0ZsZXhPU2lnbmVyVGVzdDEOMAwGA1UECgwF
+RmxleE8xDDAKBgNVBAsMA0RldjELMAkGA1UEBhMCREUwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDwAwggIKAoICAQDD/z/PnDWjAh4Vpp80MzsxD1K6vgsEV8IEsAa31a5V
+4+0BX9RfsmfNkHMR6iwVQStuOJtdBj9VivETx66DTMII6kL0l+HURMu1zv1GNwUz
+ZT4eKIbEc+uUI8bAs+Unoi42LWhKcgQV8lsi5DHsFkio7/pRPp06JHCV4fPy4aBY
+03/nOUlAC9GGfpai7wtuc8peeVDBIWJWOTrfm8hlekkMBKSAA2uqoA0ONqZzkNXd
+ZI5r0Cb0U7yaNISoo0wHG6CsnXO+CXwARwzc0iQdN4CLzmlO3kYTBbiCMnWexNzt
+4xmKWDk0JPL2hr4EqqJiAd/Fvu9WQwSjtHQMUlfvafgwK9B8RHBIuXv7W38QVPoR
+lk22o9cbfGznmsfGzoHB7wc8PR4msj8USVf4naagG/XyCpnUxXdonatxsbaS0jpi
+9ItJ/o9jTxJT5dzVARzKUZnRDR2Mma4JuBf9b+9/VTXwOJAML+4Il43UOC+ZU2Mt
+4vlJsnEIHboIgDsYkIb9xh++K62YHxrh/bDOAr91kDovBsMVVCFDOrHh2m1jkPeE
+54jBousXe7aKW/sgjwO6g2nDA+hiIE6TffmM5m7b93s7YwuoiGI+S6J3XluEHNRv
+rzZ3GRG3YW2wdgfJezm7Et6FL9jS6AmvowGFgTXcJTh1jnxkuBt/U/0cgQtFQR7F
+PwIDAQABo1MwUTAdBgNVHQ4EFgQUwQFLZ303o+o6mA/zzCKDtne/jSEwHwYDVR0j
+BBgwFoAUwQFLZ303o+o6mA/zzCKDtne/jSEwDwYDVR0TAQH/BAUwAwEB/zANBgkq
+hkiG9w0BAQsFAAOCAgEAQyzDHm8iXLCWIimclnG3vhweWf7pNiG9FkNaJo83lfIs
+skRB7tfvC1P1KNBSpKxiogPc9iIWMj48oRcRYPhrCLrPApZtViH32MmcFFizECxH
+sHjLhy2Xr4HpqW1nF3pGC3BSt0uphiIjjAxFF58Ah/LkUjftMyao6PQX/OZOqzEJ
+uIUCmsgZJuGQ0EwwAkua8eP6DyTfemg3YgqgB1BlZET4fQ7mfQpPv6hZgrGWYB5o
+FGUPRWhpE6jo18s1zUchFepW3AfYqi2Bt52vhJKu8MLbCCwM+tyjT2jcrXRbP9Gw
+dpsjrLkCY2XDBjgRJFlprFUhfPMlMH/XidVke7bBA01jnxxbsOL/FxMLFGyNW1WR
+DNbIVgTI9lTwrg88M6erJVPEfjBkZuAAigklMmOCjVAXDORdjvjhzu2HypvLXA/1
+ffX3CS8XDeLDu8i0UpxOVlPr5ax9pNjQ6LtznBLThIQ/3N3NkURLgtbs+gHMG5lY
+wNwAbMmCp6wi6QIJEOjKt5j/QEZFklKao1h8BjRvTK0vCrJvbqBRUi9bEbYzP+Z1
+6OQDKsglPjxZE5OgSbgStgYgmiZhjon45BDmn2TejYVcxdrVPIeHBc1E55Q/vrxm
+v4BVI1OJgW4Qj8SQTGEwJLWhy2FQvwaOqkjGBtItfC+qtkX2yRupOhtUI3v7f8U=
+-----END CERTIFICATE-----
Index: tests/data/testkey.pem
===================================================================
--- tests/data/testkey.pem	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
+++ tests/data/testkey.pem	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDD/z/PnDWjAh4V
+pp80MzsxD1K6vgsEV8IEsAa31a5V4+0BX9RfsmfNkHMR6iwVQStuOJtdBj9VivET
+x66DTMII6kL0l+HURMu1zv1GNwUzZT4eKIbEc+uUI8bAs+Unoi42LWhKcgQV8lsi
+5DHsFkio7/pRPp06JHCV4fPy4aBY03/nOUlAC9GGfpai7wtuc8peeVDBIWJWOTrf
+m8hlekkMBKSAA2uqoA0ONqZzkNXdZI5r0Cb0U7yaNISoo0wHG6CsnXO+CXwARwzc
+0iQdN4CLzmlO3kYTBbiCMnWexNzt4xmKWDk0JPL2hr4EqqJiAd/Fvu9WQwSjtHQM
+UlfvafgwK9B8RHBIuXv7W38QVPoRlk22o9cbfGznmsfGzoHB7wc8PR4msj8USVf4
+naagG/XyCpnUxXdonatxsbaS0jpi9ItJ/o9jTxJT5dzVARzKUZnRDR2Mma4JuBf9
+b+9/VTXwOJAML+4Il43UOC+ZU2Mt4vlJsnEIHboIgDsYkIb9xh++K62YHxrh/bDO
+Ar91kDovBsMVVCFDOrHh2m1jkPeE54jBousXe7aKW/sgjwO6g2nDA+hiIE6TffmM
+5m7b93s7YwuoiGI+S6J3XluEHNRvrzZ3GRG3YW2wdgfJezm7Et6FL9jS6AmvowGF
+gTXcJTh1jnxkuBt/U/0cgQtFQR7FPwIDAQABAoICAADuGihrDloarXfe1YyS3aoK
+75KRSk4X+IS7LRz8N5qSvVIvWTunBhUUpWclXFYxe/pG7H3RhMVsJl64qNxYpecS
+7YRpoBm4xdq0A8GsiyrGRTgxawpNnoWSceQCNoksnNmG96K6zcgo4UPWH2KGbIvY
+r7BpAqf++kXLz7OMXI1vW4EGZZGXPMRNn3tgdQZuKDywiLCR7vyHwv7Cp33LycLz
+1rAA1Cb4IXe6zKlpu4oUaxSZ5UVtjEyTCTofpEG4YNfQ23bAZsU9kRMPMDqvQ73m
+GdvU9DIs6cY8ZRB+0KmUVU+xwXu83SZMV2SARsr+RUiT8mDPaps4RQC191Lcht/5
+g3An9VXhk7sRJcKwvAe/nZgY5b9GP7LKcSQtul1L2bp1r3rEIRtKJBoUAxuORbCO
+vg6nsu4G2FFbbk9RjxqBt4adP7KxjxBFC0ts7I7NvtxGB11Y7Vkr/za/ifksEyn0
+4somLACVeaU8KgZLuRMvQK/yt2PkQ+sxpup/UOTyoQus2jocw/h/Bk+dJjB8p6lx
+Cs0EIynj68Ffd69kq5YKlK0h78pDRe/Bg+VvMPGls0DtuRodQHuggDgHRIbyJasf
+yGuWrwluWUDNh1QSkQXX675zvaMPA60WkpHB8fh7KjKPxboeSrFAklSVSeQ3OHDy
+kOOSTeZEU+9DU6AZTcdZAoIBAQDxqjGmVge8W8mKpSrRERt3J4RXc/VZlpJDuViD
+QC/rTWRnRtURxk4u6SV3S2w6ZyKW/TneYB40AhatfBglZfs7sj14S7qzVCCxk/rD
+HU1ybXoINgcSFx+BnlLl3Yob/xWAq90mEK/ded5avH47kq1/u7J8NPu/GavZGepi
+kPuRHC1I1yXHfEcwYnmVXYjSy7qj/iylg7pxUvw5bAI9fZsX9Bhbi2Kop+ks7WZx
+ArvhCWhuXL/hlJkCRHrdCS5tVXSLjB34u//mRbHXEVRIvO64mJG+Ar/CbsZCBeQ2
+6csf/PmKbJpvVC4rPGHsvOFbHK9NCfZpm6j4gteq968/h1DjAoIBAQDPn5EVDgno
+3c/2CPMZ9BaEHWDQODQiCVYMNJusw5u3JxEdgVbjQHRoT11YamAEwFbqdmm23eVZ
+vPHcKwuoXdE7s7wKiNnJN1goS8Iw12CiiVccQBy/0HQ3zdZerB/82P0/65AzuIF9
+zJW2jD6yQv7iHH/U0wHzxdlNUMzbUs69qZeOIjScsiUYXxcio0DUW3FWOOzZRW8z
+BpP0GIsDpccZ5ZDnIPuQN8NiYBVqPt0tkgxnbHzpc0mE443umVxQ2C1dOUSB9tFs
+NSK5ZNshY7gpKyu/aNsZClrFKfVOae1eJ70PXyPglPR94CzyvohHgoewtP4y8pu5
++V72RXKrQvT1AoIBAQC9IiEneC8nuIJccvW2l/fx4kiOFR/RFKm2PSnL1wFp27EX
+rvT0q0nnJur+mVXDw5Hrr5WJt5oLxBKxhexz4IOmbdH0AvjEfrPzpsfbymQhiRK3
+uGkCPHyY+isQ4bjEPng6sscqkmbBJC7kvp7gyuPkkyaWVPUt086N491vdN6d6/p5
+BoD3xgkFEhzrzD9YEsAotrWEeDsDlBn4atK6A0LNlWk2xDjbnEY+tjG1gpk9/xnv
+PcGir6SshKHPue7O/NFwoaGXWLNnKeaMVnCrWMpQSH/PwOEJL58Ubv30CDfD+j3m
+myamuLBFgEV3fT+2ChYcag6jCoNMs0JM+Pn781BXAoIBAQCY9Kv0f/Aq/uZLElLO
+BvVjhSUioJU/kfMzcLpTL5QS/RPt0bBKINzhT76r+UodlfkyJ+Q/lP92+eyQT6H5
++ou/WO0qMaGITF6E6TL5umH1vApRxKWpZg+IYPsRqeqy4sTHh2onwECdhc+xuWYi
++5o8x9Qg3QauKU6qV+FjnoCyVzNQwcBSxCbx6nPnz69eArPfWnaapj2CsNDk3gPj
+vwiL+oSi0biiiYJGghSkvgVBojvDIbhwX7+EyANzCMZqxKe+6waeXE/yUL7xJ+0L
+jyRIKAbFr6DhtLAkWoHer8jwOYRdw7BxrirkZPeYKWfKjs4aD2zJ6dNx0dX0xjrx
+EpXVAoIBAC8Js0CkPgH82bp7tfekvRLfYUJPCvf8mL2F0gJrTDj2H9D7nrNiOMw6
+y2ekY6t9EE2ivc1KRQsscUv45ZpZDqoxaExdCYtkt4kyYripIFPKrFklFwoe3hCZ
+6NJwL9KHjCZ0SRDvImnMvOzkPSt2Ki7EfezptTj4LQdZXOfSY8TR1P07lrU/I+Z6
+eD1AkqJ1xogu//Te7k7et/z3nv89Wo5+9mngW4Wt9+Z18y4LomOyoMfpUFR+3ORN
+SLZkMzYOVEXiuTI7vt1XcKbQik9L5DHSQUzbYoYJXBV98S3EYEODZuMZbeohk6lH
+COTzylGJM5V2427v9SqnT+ic1kCvejw=
+-----END PRIVATE KEY-----
Index: tests/test_signing.py
===================================================================
--- tests/test_signing.py	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
+++ tests/test_signing.py	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -0,0 +1,63 @@
+import platform
+import pytest
+from uuid import uuid4
+from flexoentity import FlexOID, EntityState, EntityType, FlexoSignature, get_signing_backend
+
+def test_sign_and_verify_linux(backend):
+    data = b"Hello Flex-O signing!"
+
+    signature = backend.sign(data)
+    assert isinstance(signature, bytes)
+    assert len(signature) > 20      # sanity check
+
+    assert backend.verify(data, signature) is True
+
+def test_sign_and_verify_macos(backend):
+    data = b"Hello Flex-O signing!"
+
+    signature = backend.sign(data)
+    assert isinstance(signature, bytes)
+    assert len(signature) > 20      # sanity check
+
+    assert backend.verify(data, signature) is True
+
+def test_verify_fails_with_wrong_data(backend):
+    data = b"Original Data"
+    wrong_data = b"Tampered Data"
+
+    signature = backend.sign(data)
+
+    assert backend.verify(data, signature) == True
+    assert backend.verify(wrong_data, signature) == False
+
+def test_verify_fails_with_invalid_signature(backend):
+    data = b"Hello world"
+    invalid_sig = b"\x00\x01\x02garbagepkcs7data"
+
+    assert backend.verify(data, invalid_sig) is False
+
+def test_signature_entity_create_and_verify(backend):
+    entity_id = FlexOID.safe_generate(
+        domain_id="TEST",
+        entity_type=EntityType.ATTESTATION.value,
+        state=EntityState.DRAFT.value,
+        text="abc",
+        version=1,
+    )
+    signer = uuid4()
+    data = b"Hello Entity Signing"
+
+    sig = FlexoSignature.create_signed(
+        data=data,
+        entity=entity_id,
+        signer_id=signer,
+        backend=backend,
+    )
+
+    assert isinstance(sig.signature_data, str)
+    assert sig.signature_type == "PKCS7-DER"
+    assert sig.signed_entity == entity_id
+    assert sig.signer_id == signer
+    assert sig.certificate_thumbprint != ""
+
+    assert sig.verify(data, backend) is True
