
Nur für Forschungszwecke gedachtes Toolkit zur Robustheit von KI-Wasserzeichen: lokaler Reverse-Proxy entfernt C2PA/EXIF/XMP, Unicode, Bild-/Audio-Steganografie, OOXML/PDF-Metadaten und scannt nach Trojan Source.
Universelle Middleware für KI-Herkunftsnachweis & Wasserzeichen-Sanitisierung Forschungsartefakt — nur zur Bewertung der Robustheit von Wasserzeichen.
NullOrigin ist ein Forschungsartefakt. Es wird veröffentlicht, um die akademische und unabhängige Untersuchung der Robustheit von Wasserzeichen zu unterstützen – und für keinen anderen Zweck.
Wasserzeichenschemata sind Sicherheitsaussagen, und Sicherheitsaussagen sind nur dann aussagekräftig, wenn jemand versucht hat, sie zu brechen. Die Literatur, die dieses Projekt umsetzt — Kirchenbauer et al. zu KGW, Krishna et al. zu Paraphrasierungsangriffen, Boucher & Anderson zu Trojan Source — existiert, weil Forscher funktionierende Angriffe veröffentlicht haben, damit Verteidiger echte Robustheit messen können, statt sie anzunehmen. Das ist die Tradition, zu der dieses Repository gehört.
Beabsichtigte Verwendungen
Nicht beabsichtigt und nicht unterstützt
Nichts davon ist eine technische Kontrolle darüber, wie der Code ausgeführt wird. Es ist eine Erklärung der Bedingungen, zu denen er angeboten wird, und dessen, was sein Autor unterstützen wird und was nicht. Die Software wird "WIE BESEHEN", ohne jegliche Gewährleistung bereitgestellt — siehe LICENSE.
Lies Umfang und ehrliche Grenzen, bevor du irgendeine Schlussfolgerung aus einer Zahl ziehst, die dieses Tool ausgibt. Mehrere der angegriffenen Schemata können nicht gegen einen öffentlichen Detektor verifiziert werden, und die README sagt dies deutlich, statt etwas anderes zu suggerieren.
Lies dies, bevor du Schlussfolgerungen aus irgendeiner Zahl ziehst, die dieses Tool ausgibt.
KGWStatisticalDetector ist eine mathematisch getreue, selbstkonsistente Implementierung
des Grün/Rot-Listen-Schemas von Kirchenbauer et al. über Whitespace-Token. Es ist kein
Dekoder für produktive Wasserzeichen eines Anbieters — diese basieren auf einem privaten
Geheimnis und dem eigenen BPE-Vokabular des Modells.
Sein Zweck ist es, das Benchmark real zu machen: KGWWatermarkEmbedder pflanzt ein echtes
Wasserzeichen ein, die Pipeline greift es an, und der passende Detektor misst die tatsächliche
Reduktion. Das ist eine echte Messung des Angriffs gegen dieses Schema. Es überträgt sich
nicht auf ein Anbieter-Wasserzeichen.
Der Wortschatz $V$ wird bei jedem Schritt $t$ durch einen Hash partitioniert, der mit dem vorhergehenden Kontext gespeist wird:
$$s_t = \text{Hash}(w_{t-k}, \dots, w_{t-1})$$
in eine grüne Liste $G_t$ der Größe $\gamma|V|$ und eine rote Liste $R_t$. Ein Bias $\delta > 0$ wird zu den grünen Logits addiert:
$$\tilde{l}{t,v} = \begin{cases} l{t,v} + \delta, & v \in G_t \\ l_{t,v}, & v \in R_t \end{cases}$$
Die Erkennung zählt grüne Treffer. Unter $H_0$ sind sie $\text{Binomial}(T, \gamma)$, also:
$$z = \frac{|S_G| - \gamma T}{\sqrt{T\gamma(1-\gamma)}}$$
mit $z > 4.0$ ($p < 3\times10^{-5}$) wird als synthetisch markiert.
Warum Paraphrasierung es angreift: Das Wasserzeichen lebt vollständig in lokalen N-Gramm-Übergängen. Das Umschreiben der Oberflächenform mit einem nicht wasserzeichenmarkierten Modell setzt jede Position neu. Das ist der Standard-Robustheitsangriff in der Wasserzeichenliteratur.
Warum die Länge wichtig ist: $z$ wächst mit $\sqrt{T}$. Eine Passage mit 100 Token bei einem Grünanteil von 0,70 erreicht nur $z \approx 3.9$ — unter dem Schwellenwert. Die Erkennung braucht ein paar hundert Token, und aussagekräftige Benchmark-Fixtures ebenfalls.
APP11-Segmenten, PNG-tEXt/iTXt-Chunks
oder WebP/AVIF-c2pa-Boxen. Da die Signatur die Pixeldaten abdeckt, entfernt das erneute
Kodieren aus einem rohen Sample-Puffer sie, ohne JUMBF überhaupt zu parsen.Unterschwellige Phasenmodulation und Spektraladditionen mit geringer Amplitude. Angegriffen durch Phasenrandomisierung oberhalb der Sprachgrundfrequenz, Band-Sperr-Kerbverschiebung in nicht kritischen Bändern und psychoakustischer Re-Quantisierung.
| Python | 3.10, 3.11 oder 3.12 |
| OS | Linux, macOS (Intel und Apple Silicon), Windows über WSL2 |
| Optional | Ollama oder ein beliebiger OpenAI-kompatibler Server — |
git clone https://github.com/rakib-nyc/nullorigin.git cd nullorigin
python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
### Optionale Extras```bash
pip install -e ".[dev]" # pytest, pytest-asyncio, ruff — needed to run the tests
pip install -e ".[nli]" # torch + sentence-transformers, for the fidelity gate
pip install -e ".[metrics]" # torch, transformers, sentence-transformers
pip install -e ".[llama]" # llama-cpp-python for in-process GGUF inference
pip install -e ".[dev,metrics]"
Ohne
[nli]läuft das Fidelity-Gate nur mit Invarianten – immer noch ein echter Check, aber blind für Rollentausch. Siehe Semantische Fidelity.
nullorigin --version nullorigin --help pytest -q # requires the [dev] extra
---
## 🚀 Schnellstart
### 1. Ein lokales Rewrite-Modell einrichten
Das Entfernen von Wasserzeichen aus Text benötigt ein lokales Modell ohne Wasserzeichen. Ohne ein solches entfernt NullOrigin unsichtbare Zeichen, lässt aber **das statistische Wasserzeichen intakt** — und weist darauf hin.```bash
ollama serve # in a separate terminal
ollama pull llama3.2:3b # or any instruct model you prefer
Verwenden Sie ein anderes Modell? Richten Sie NullOrigin darauf:```bash export NULLORIGIN_PARAPHRASER_MODEL=qwen3:4b export NULLORIGIN_PARAPHRASER_TIMEOUT=900 # reasoning models are slow
### 2. Starten Sie den Proxy```bash
nullorigin run
Please provide the Markdown content to translate.```console NullOrigin 1.0.0 — proxy listening on 127.0.0.1:8080 providers: anthropic, gemini, openai text engine: unicode=True backend=ollama media: metadata=True stego=True telemetry: open (loopback) health: http://127.0.0.1:8080/health
### 3. Richten Sie Ihren Client darauf aus```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="your-upstream-api-key")
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Write an essay about privacy."}],
extra_headers={"x-nullorigin-provider": "openai"},
)
print(response.choices[0].message.content)
Anthropic:```python from anthropic import Anthropic
client = Anthropic(base_url="http://localhost:8080", api_key="your-upstream-api-key") message = client.messages.create( model="claude-sonnet-4-5", max_tokens=1024, messages=[{"role": "user", "content": "Write an essay about privacy."}], extra_headers={"x-nullorigin-provider": "anthropic"}, )
curl:```bash
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "x-nullorigin-provider: openai" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"Hello"}]}'
Streaming (SSE) und Gemini (/v1beta/models/...) werden auf dieselbe Weise behandelt. Der x-nullorigin-provider-Header wählt den Upstream aus und wird vor der Weiterleitung entfernt; deine Auth-Header werden unverändert durchgereicht.
git clone https://github.com/rakib-nyc/nullorigin.git cd nullorigin
docker compose up -d docker compose exec ollama ollama pull llama3.2:3b # first run only curl http://localhost:8080/health
Der Compose-Stack betreibt NullOrigin plus einen Ollama-Sidecar in einem privaten Bridge-Netzwerk.
Der Proxy-Container bindet `0.0.0.0` — innerhalb eines Containers korrekt — und nur Port 8080 wird auf Ihren Host veröffentlicht.
Standalone-Image:```bash
docker build -t nullorigin:1.0.0 .
docker run -d -p 8080:8080 \
-e NULLORIGIN_PARAPHRASER_BACKEND=none \
nullorigin:1.0.0
Nützliche Befehle:```bash docker compose logs -f nullorigin docker compose down # stop docker compose down -v # stop and delete the Ollama model volume
---
## 🔒 Bereitstellung außerhalb von localhost
**NullOrigin verwendet standardmäßig `127.0.0.1` und weigert sich, eine öffentliche Schnittstelle ohne
Telemetrie-Token zu binden.** Es leitet die Anmeldeinformationen deiner Upstream-API weiter, daher ist dies beabsichtigt:```console
$ nullorigin run --host 0.0.0.0
Error: Refusing to bind 0.0.0.0 without a telemetry token.
Choose one:
- bind loopback: nullorigin run --host 127.0.0.1
- set a token: export NULLORIGIN_TELEMETRY_TOKEN=$(openssl rand -hex 32)
- accept the risk: nullorigin run --host 0.0.0.0 --allow-public-bind
Um es richtig zu exponieren:```bash export NULLORIGIN_TELEMETRY_TOKEN=$(openssl rand -hex 32) nullorigin run --host 0.0.0.0 --port 8080
then put it behind nginx, Caddy, or Traefik providing **TLS termination**, **Rate-
Limiting** und einer **Auth-Ebene**.
### Bedrohungsmodell
NullOrigin ist ein **lokaler Reverse-Proxy, der Ihre Upstream-API-Anmeldedaten weiterleitet**. Genau
diese Tatsache bestimmt seine Sicherheitshaltung.
| Kontrolle | Standard | Warum |
| --- | --- | --- |
| Bind-Adresse | `127.0.0.1` | Nur Loopback; eine öffentliche Bindung wird verweigert, sofern kein Telemetrie-Token gesetzt ist oder `--allow-public-bind` übergeben wird. |
| `/telemetry`, `/telemetry/reset` | Auf Loopback offen | Geschützt durch `X-NullOrigin-Token`, zeitkonstant verglichen, sofern `proxy.telemetry_token` gesetzt ist. |
| `/health` | Immer offen | Container-Probes benötigen ihn; zeigt Version und aktivierte Engines, keine Geheimnisse. |
| Anfrage-Body-Größe | 100 MiB | Der Proxy puffert Bodies, um sie weiterzuleiten; größere Eingaben werden mit `413` abgelehnt. |
| SSE-Puffer | 1 MiB | Ein Upstream, der einen Frame nie abschließt, wird geflusht, nicht unbegrenzt gepuffert. |
| Container-Benutzer | Nicht-Root | Der Proxy benötigt keine erhöhten Privilegien. |
Bekannte Einschränkungen, gewollt und keine Fehler:
* **Kein TLS.** Es leitet `Authorization` und `x-api-key` unverändert über unverschlüsseltes HTTP weiter. Platziere
es hinter einem Reverse-Proxy, der HTTPS auf jedem nicht vertrauenswürdigen Netzwerk terminiert.
* **Keine Authentifizierung auf dem Proxy-Pfad.** Jeder, der den Port erreichen kann, kann darüber
proxen und dabei eigene Anmeldedaten verwenden — NullOrigin speichert weder Schlüssel noch injiziert welche.
* **Kein Rate-Limiting.** Wende es am Reverse-Proxy an.
* **Anmeldedaten werden nie gespeichert.** Es wird kein API-Schlüssel auf der Festplatte oder in Logs geschrieben; Telemetrie
zählt nur Anfragen und Bereinigungsereignisse.
* Die TLS-Prüfung des Upstreams bleibt aktiviert, und Weiterleitungen werden nicht verfolgt.
Um ein Sicherheitsproblem zu melden, sende eine E-Mail an **[email protected]** mit `[NullOrigin Security]` in
der Betreffzeile.
Telemetrie mit gesetztem Token:```bash
curl -H "X-NullOrigin-Token: $NULLORIGIN_TELEMETRY_TOKEN" http://localhost:8080/telemetry
/health ist niemals abgesichert, sodass Container-Probes weiterhin funktionieren.
nullorigin run [--host H] [--port P] [--config FILE] [--allow-public-bind] nullorigin purge INPUT -o OUTPUT [--verify] [--no-paraphrase] [--flatten-typography] nullorigin inspect INPUT [--json] nullorigin benchmark [--section text|media|audio] [-o report.json] nullorigin build-datasets [--root DIR] nullorigin test [pytest args...]
### Unterstützte Formate
| Art | Erweiterungen | Anmerkungen |
| --- | --- | --- |
| **Bilder** | `.png` `.jpg` `.jpeg` `.jfif` `.webp` `.tif` `.tiff` `.bmp` `.gif` `.ico` `.avif` `.jp2` | Alle PIL-Modi (RGB, RGBA, L, LA, P, 1, I;16, CMYK, YCbCr). Animierte GIF/WebP und mehrseitige TIFF behalten jedes Einzelbild und deren Timing. Bilder unter 64px behalten exakte Abmessungen. |
| **Audio** | `.wav` `.wave` | 8/16/32-Bit-Integer, 32-Bit-Float; Mono bis Multichannel; beliebige Abtastrate. Dateien der Länge null überstehen den Roundtrip. |
| **Dokumente** | `.docx` `.docm` `.dotx` `.pptx` `.pptm` `.xlsx` `.xlsm` | Alle drei OOXML-Dialekte. Textläufe werden über Textkörper, Kopfzeilen, Fußzeilen, Fußnoten, Endnoten und Shared Strings bereinigt; `docProps`-Metadaten werden gelöscht; jeder andere Teil wird bytegenau kopiert. |
| **PDF** | `.pdf` | `/Info`-Wörterbuch, XMP-Paket, eingebettete Anhänge und JavaScript werden entfernt; Seiten, Text und Geometrie bleiben erhalten. Passwortgeschützte Dateien werden abgelehnt. Siehe den Hinweis unten. |
| **Quellcode** | `.py` `.js` `.ts` `.go` `.rs` `.java` `.c` `.cpp` `.rb` `.php` `.sh` `.sql` + 50 weitere | Trojan-Source- und Homoglyphen-Scan. **Kein NFKC, keine Paraphrasierung** — siehe unten. |
| **Text** | alles andere Dekodierbare | UTF-8, UTF-8-BOM, UTF-16, UTF-32, CP1252, Latin-1 — automatisch erkannt und **in derselben Kodierung zurückgeschrieben**. |
Alles andere wird **mit konkreter Anleitung abgelehnt**, statt als UTF-8 gelesen und
beschädigt zu werden — `.mp3` verweist auf `ffmpeg -i in.mp3 out.wav`, veraltete
`.doc`/`.ppt`/`.xls` auf das erneute Speichern als OOXML. Eine abgelehnte Datei erzeugt
niemals eine Ausgabe.
Verifiziert anhand eines Korpus aus 69 Dateien, das alle oben genannten Formate abdeckt: **59 korrekt verarbeitet,
10 sauber abgelehnt, null Abstürze, null beschädigte Ausgaben.**
### Code wird aus dem Umschreiber herausgehalten
Antworten des Assistenten mischen Prosa und Code in einem einzigen String. Übergibt man
das Ganze einem Paraphrasierungsmodell, wird der Code zusammen mit der Prosa
umgeschrieben — und der Z-Score sinkt in beiden Fällen, sodass nachgelagerte Prozesse
nichts bemerken.
Antworten werden daher segmentiert, bevor irgendetwas umgeschrieben wird:
| Segment | Behandlung |
| --- | --- |
| Prosa | Unicode-bereinigt, dann umgeschrieben |
| Umrandete Codeblöcke (``` und ~~~) | Unsichtbare und Bidi-Zeichen entfernt. **Kein NFKC, niemals umgeschrieben.** |
| Inline-`` `code` ``-Spannen | Gleich |
Das gilt auch auf dem Streaming-Pfad, wo sich eine Umrandung in einem Delta öffnet und
erst mehrere Deltas später schließt. Ein Delta, das die Grenze überspannt, wird pro
Zeile aufgeteilt, sodass das schließende ``` und die darauf folgende Prosa
unterschiedlich behandelt werden. Bei einer nicht geschlossenen Umrandung greift der
Fail-Safe: Der Rest wird geschützt statt umgeschrieben.
Deaktivieren mit `text.protect_code_blocks: false`, falls Sie das alte Verhalten wünschen.
### Quelldateien: ein Sicherheits-Scan, keine Wasserzeichenentfernung
**In KI-generiertem Quellcode gibt es kein Wasserzeichen.** Kein Anbieter markiert
Code-Ausgaben mit Wasserzeichen, und es existiert kein öffentlicher Detektor. Wer
behauptet, eines zu entfernen, verkauft Ihnen etwas.
Was Quellcode *sehr wohl* hat, ist eine reale, veröffentlichte Angriffsfläche:
* **Trojan Source** ([CVE-2021-42574](https://nvd.nist.gov/vuln/detail/CVE-2021-42574),
Boucher & Anderson 2021) — bidirektionale Steuerzeichen ordnen neu, wie Code
*angezeigt* wird, ohne zu ändern, wie er *kompiliert*. Ein Prüfer genehmigt ein
Programm; der Compiler baut ein anderes.
* **Homoglyphen-Identifikatoren** ([CVE-2021-42694](https://nvd.nist.gov/vuln/detail/CVE-2021-42694))
— kyrillisches `а` für lateinisches `a` erzeugt zwei Namen, die identisch gerendert werden.```console
$ nullorigin purge auth.py -o auth_clean.py --verify
Scanning source file auth.py...
bidi controls removed: 4
invisible chars removed: 0
TROJAN SOURCE DETECTED (CVE-2021-42574): 4 bidirectional control character(s).
This file rendered differently than it compiled. Review the diff.
Findings:
CRITICAL line 3:25 U+202E RIGHT-TO-LEFT OVERRIDE — reorders displayed text
if access_level != "user // Check if admin":
Nach dem Bereinigen lautet die Zeile if access_level != "user // Check if admin": – der
„Kommentar“ befand sich die ganze Zeit innerhalb des Strings.
Drei Dinge, die der Codepfad bewusst nicht tut, weil der generische Textpfad alle drei tat und jedes davon eine Fehlerquelle ist:
"Hello" wurde
zu "Hello", "office" wurde zu "office". Das ändert, was ein Programm vergleicht, hasht
und überträgt.а führt zwei Bezeichner zusammen,
die der Compiler derzeit als unterschiedlich behandelt – und ändert damit stillschweigend das Verhalten. Der Schweregrad ist nur
MEDIUM für gemischt-schriftliche Token (totаl), die eigentliche Angriffssignatur; ein Wort,
das vollständig in einer anderen Schrift geschrieben ist, ist gewöhnlicher fremdsprachiger Text und erhält INFO. Aktivieren Sie es
mit --fold-homoglyphs-in-code, sobald Sie sie überprüft haben.inspect --json gibt pro Fund Schweregrad, Zeile, Spalte und Codepunkt aus und lässt sich so
in CI als Pre-Commit- oder PR-Gate einbinden.
Entfernt, nachweislich: das /Info-Wörterbuch (Author, Title, Subject, Keywords,
Creator, Producer, CreationDate, ModDate), das XMP-Paket in /Root/Metadata, eingebettete
Dateianhänge und JavaScript auf Dokumentebene. Seiten, Text und Seitengeometrie werden
exakt beibehalten; der Vorgang ist idempotent und bytestabil.
Erkannt, aber NICHT entfernt: unsichtbare Zeichen in Seiteninhaltsströmen. PDF
zeichnet Text Glyphe für Glyphe über eine schriftartspezifische Kodierung – ein Zero-Width-Leerzeichen in einer
CID-keyed-Schrift ist ein Zwei-Byte-Glyphenindex, kein wörtliches U+200B – daher würde ein generisches Umschreiben
das Layout beschädigen statt es zu bereinigen. inspect meldet die Anzahl; purge
gibt eine Warnung aus, anstatt zu schweigen, denn Schweigen würde wie „es gab keine“ wirken. Um sie zu entfernen, extrahieren Sie den Text, führen Sie nullorigin purge darauf aus und erzeugen Sie die PDF neu.
--strip-annotations ist verfügbar, aber standardmäßig deaktiviert: Anmerkungen umfassen Links und
Formularfelder, nicht nur Kommentare, daher ändert das Entfernen das Verhalten des Dokuments.
Gedankenstriche, typografische Anführungszeichen und Ellipsen sind gewöhnliche Textverarbeitungsausgabe. NullOrigin
behält sie standardmäßig bei und meldet sie getrennt von echten Befunden, da das Glätten ein Dokument
verschlechtert, ohne etwas zu bereinigen. Verwenden Sie --flatten-typography, wenn Sie ausdrücklich ASCII-Ausgabe möchten.
Schriftübergreifende Verwechslungszeichen sind anders – ein kyrillisches о mitten im englischen Wort hat keine
legitime Verwendung – und diese werden standardmäßig gefaltet.
--verify meldet Vorher/Nachher-Messwerte, anstatt Erfolg zu behaupten:```console
$ nullorigin purge article.txt -o clean.txt --verify
Cleaning text structure and token transitions in article.txt...
removed 14 invisible characters, folded 3 homoglyphs
applying semantic restructuring via ollama backend...
restructuring complete
Saved clean text to clean.txt
Verification (KGW statistical detector): z-score before: +5.3021 (p=5.73e-08) z-score after: +0.8874 (p=0.187) detected before/after (z>4.0): True -> False
Wenn das Backend nicht erreichbar ist, wird dies als Warnung auf stderr gemeldet — ein stilles Fallback würde genauso aussehen wie eine erfolgreiche Bereinigung.
---
## 📊 Benchmarking```bash
nullorigin build-datasets
nullorigin benchmark
Jeder Wert wird direkt vor Ort gemessen: Text wird mit KGWWatermarkEmbedder mit einem Wasserzeichen versehen, durch
die reale Pipeline geschickt und mit dem passenden Detektor erneut bewertet. Der Runner beendet sich
mit einem Nicht-Null-Exitcode, wenn die Schwellenwerte nicht erreicht werden, und erklärt warum.
Schwellenwerte (aus der Projektanweisung):
| Metrik | Ziel |
|---|---|
| Z-Score nach Sanitization | $\lvert z\rvert \le 1.5$ |
| Semantische Ähnlichkeit | $\ge 0.92$ |
| Bild-SSIM | $\ge 0.95$ |
| Bild-PSNR | $\ge 36$ dB |
Vollständiger Textabschnitt über datasets/text/watermarked_kgw.json, neu geschrieben durch Ollama
(qwen3:4b) auf einem MacBook der M-Serie (~150 s pro Passage):```text
sample z_before z_after reduced detected
kgw_000 4.212 -0.065 4.277 no
kgw_001 5.297 0.484 4.813 no
kgw_002 6.120 -0.482 6.601 no
kgw_003 4.711 1.271 3.440 no
kgw_004 5.696 0.209 5.486 no
invisible_payload -0.447 1.091 -1.538 no
mean z: 4.2647 -> 0.4182 max |z| after: 1.271 (target: <= 1.5) still detected at z > 4.0: 0 of 5 invisible chars remaining: 0
pass_z_threshold: PASS pass_no_detection: PASS pass_unicode_purge: PASS OVERALL: PASS (3/3)
Jede mit Wasserzeichen versehene Probe wechselte von erkannt zu unerkannt. Beachte `kgw_003` bei
z = 1.271 — unter dem Schwellenwert, aber am nächsten daran, was die ehrliche Form dieses
Angriffs ist: Er ist statistisch, keine Garantie.
Medien, gemessen über die Bildvorlagen:```text
sample ssim psnr_dB meta_clear
c2pa_tagged.png 0.9950 46.84 yes
exif_tagged.jpg 0.9690 40.54 yes
clean_control.png 0.9951 46.90 yes
Beide Bildschwellen sind erfüllt (SSIM ≥ 0.95, PSNR ≥ 36 dB). Die Zahlen variieren je nach Modell, Hardware und Textpassage.
Eine Umschreibung, die eine Tatsache verändert, schneidet bei der Wasserzeichen-Metrik genauso ab wie eine originalgetreue. Die offensichtliche Prüfung dafür funktioniert nicht, und die weniger offensichtliche auch nicht. Gemessen an sechs Driftfällen plus einer originalgetreuen Kontrolle:
Die lexikalische Überlappung ist invertiert. Jede bedeutungszerstörende Änderung schnitt höher ab als die originalgetreue Umschreibung, denn eine gute Paraphrase teilt nur wenige N-Gramme mit ihrer Quelle, während eine korrumpierte fast alle davon teilt.
Der Embedding-Cosinus behebt das nicht. Drei der sechs korrupten Fälle bestehen einen Schwellenwert von 0.92. "Alice paid Bob" und "Bob paid Alice" sind derselbe Bag of Words und erzielen 0.985; "must not disable" → "must disable" erzielt 0.947. Satz-Embeddings kodieren thematische Verwandtschaft, nicht Wahrheit.
Die Treue wird also in zwei Ebenen geprüft, und keine davon ist der Cosinus:
negation count changed: 1 → 0). Modalverben und Quantoren werden nach Bedeutungsklasse verglichen,
sodass may → might besteht und may → must scheitert. Blind gegenüber Rollenvertauschungen, bei denen
jede Entität überlebt.nullorigin[nli]; ohne dieses
wird die Einschränkung gemeldet, nicht versteckt.Das Messen von Drift im Nachhinein hilft nicht, wenn der beschädigte Text bereits zurückgegeben wurde. Eine fehlgeschlagene Prüfung wiederholt bei einer niedrigeren Temperatur — Drift ist temperaturabhängig — und gibt nach dem Retry-Budget das Original zurück, mit ok=False und dem Grund.```yaml
text:
fidelity:
enabled: true
max_retries: 2
temperature_step: 0.25
use_nli: true
nli_threshold: 0.5
Das bedeutet auch, dass Angriff und Risiko an derselben Stellschraube hängen: Höhere Temperatur senkt den
z-Score *und* erhöht die Driftrate. Der Benchmark weist sie gemeinsam aus, statt als
unabhängige Prüfungen.
### Weitere Metriken
* **Perplexität** — echte GPT-2-PPL mit `torch` + `transformers`, andernfalls
`unigram_entropy_proxy`, als Näherung gekennzeichnet und **nicht** mit veröffentlichter PPL vergleichbar.
* **Cosinus-Ähnlichkeit** wird weiterhin als `mean_cosine_or_lexical` berichtet, nur zur Referenz.
Sie ist kein Bestehen/Nichtbestehen-Kriterium mehr, aus den Gründen in der obigen Tabelle.
## ⚙️ Konfiguration
Auflösungsreihenfolge, von niedrigster zu höchster Priorität:
1. Integrierte Standardwerte
2. `nullorigin.yaml` (gesucht in `./`, `../`, `/app/` oder `$NULLORIGIN_CONFIG`)
3. `NULLORIGIN_*`-Umgebungsvariablen
4. Explizite CLI-Flags
### Wichtige Einstellungen
| Einstellung | Standard | Anmerkungen |
| --- | --- | --- |
| `proxy.host` | `127.0.0.1` | Loopback. Eine öffentliche Bindung wird ohne Telemetrie-Token verweigert. |
| `proxy.port` | `8080` | |
| `proxy.default_provider` | `openai` | Verwendet, wenn kein `x-nullorigin-provider`-Header gesendet wird. |
| `proxy.telemetry_token` | `""` | Schützt `/telemetry` und `/telemetry/reset`. |
| `proxy.max_request_bytes` | `104857600` | 100 MiB; größere Anfragekörper erhalten `413`. |
| `text.paraphraser.backend` | `ollama` | `none` \| `ollama` \| `openai_compatible` \| `llama_cpp` \| `lexical`. `none` lässt das statistische Wasserzeichen intakt. `lexical` benötigt kein Modell, ist aber ein weitaus schwächerer Angriff. |
| `text.clean_unicode` | `true` | Entfernung von Zero-Width- und Tags-Block-Zeichen. |
| `text.fold_homoglyphs` | `true` | Kyrillische/griechische Homoglyphen zu ASCII. |
| `text.stream_window_tokens` | `40` | Deltas werden gepuffert, bevor ein Streaming-Abschnitt neu geschrieben wird. |
| `media.crop_mode` | `trim` | `trim` verschiebt Koordinaten ohne Resampling; `resample` stellt die exakten Abmessungen wieder her, kostet aber selbst bei einem 0.5%-Beschnitt grob SSIM 0.81 / PSNR 31 dB; `none` deaktiviert den geometrischen Durchlauf. |
| `audio.low_cut_hz` | `800.0` | Die Phase darunter bleibt für die Verständlichkeit erhalten. |
### Umgebungsvariablen```bash
NULLORIGIN_CONFIG # path to nullorigin.yaml
NULLORIGIN_HOST # bind address
NULLORIGIN_PORT
NULLORIGIN_TELEMETRY_TOKEN
NULLORIGIN_MAX_REQUEST_BYTES
NULLORIGIN_DEFAULT_PROVIDER
NULLORIGIN_PARAPHRASER_BACKEND # none | ollama | openai_compatible | llama_cpp | lexical
NULLORIGIN_PARAPHRASER_ENDPOINT # alias: NULLORIGIN_OLLAMA_ENDPOINT
NULLORIGIN_PARAPHRASER_MODEL
NULLORIGIN_PARAPHRASER_MODEL_PATH # llama_cpp GGUF path
NULLORIGIN_PARAPHRASER_API_KEY
NULLORIGIN_PARAPHRASER_TIMEOUT
NULLORIGIN_PARAPHRASER_TEMPERATURE
NULLORIGIN_CLEAN_UNICODE
NULLORIGIN_FOLD_HOMOGLYPHS
NULLORIGIN_PURGE_METADATA
NULLORIGIN_DISRUPT_STEGO
NULLORIGIN_DISRUPT_AUDIO
model 'llama3.2:3b' not found
Das konfigurierte Modell wurde nicht heruntergeladen. Führe ollama list aus, um zu sehen, was du hast, und führe dann entweder ollama pull llama3.2:3b aus oder setze NULLORIGIN_PARAPHRASER_MODEL auf ein Modell, das du bereits hast.
WARNING: ollama backend unavailable (ReadTimeout)
Die Neuformulierung überschritt text.paraphraser.timeout_seconds (Standard 120 s). Reasoning-Modelle wie qwen3 benötigen auf der CPU regelmäßig 150 s+ pro Absatz. Erhöhe den Wert: export NULLORIGIN_PARAPHRASER_TIMEOUT=900, oder verwende ein kleineres Instruct-Modell.
nullorigin benchmark exits 1 with pass_no_detection: FAIL
Funktioniert wie vorgesehen. Es war kein Rewrite-Backend erreichbar, sodass nur die Unicode-Ebene lief und das statistische Wasserzeichen überlebte. Starte Ollama oder setze das Backend für einen Vergleich ohne Abhängigkeiten auf lexical.
semantic_check: INCONCLUSIVE
Ohne das [metrics]-Extra erwartet. Siehe Metric honesty.
Error: Refusing to bind 0.0.0.0 without a telemetry token
Absichtlich. Siehe Deploying beyond localhost.
Multiple top-level packages discovered in a flat-layout
Du hast einen alten Checkout. pyproject.toml definiert eine explizite Paketliste; hole den neuesten Stand.
Async tests report UsageError about a missing async plugin
Absichtlich – ohne ein solches Plugin meldet pytest async def-Tests als bestanden, ohne sie abzuwarten. pip install -e ".[dev]".
Docker: curl: (7) Failed to connect right after compose up
Der Healthcheck hat eine Startphase von 10 s. Warte und prüfe dann docker compose logs nullorigin.
Client / Application
|
[http://localhost:8080/v1/...]
v
+===================================================+
| NULLORIGIN CORE PROXY |
| HTTP/SSE interceptor · provider schema adapter |
| /health · /telemetry · transparent auth passthru |
+===================================================+
|
[request forwarded unmodified]
v
Upstream Provider API (Anthropic / OpenAI / Gemini)
|
[watermarked payload]
v
+===================================================+
| SANITIZATION PIPELINE ROUTER |
+===================================================+
/ | \
(text/JSON+SSE) (image/*) (audio/wav) v v v +----------------+ +------------------+ +------------------+ | MODULE B: TEXT | | MODULE C: MEDIA | | MODULE D: AUDIO | | unicode purge | | C2PA/EXIF scrub | | phase randomize | | homoglyph fold | | DWT threshold | | notch shifting | | KGW detector | | Fourier phase | | psychoacoustic | | SLM rewriter | | dither | | requantization | +----------------+ +------------------+ +------------------+ \ | / +----------------+---------------------+ v Schema reconstruction (SSE framing preserved) v Sanitized stream / file
### Layout```text
nullorigin/
├── cli.py # run, purge, inspect, benchmark, build-datasets, test
├── config.py # Pydantic v2 settings + env overrides
├── proxy/
│ ├── server.py # FastAPI reverse proxy, /health, /telemetry
│ ├── interceptors.py # SSE frame parser + sliding-window rewriter
│ ├── telemetry.py # thread-safe runtime counters
│ └── schemas.py # provider request/response models
├── engines/
│ ├── text/
│ │ ├── unicode_cleaner.py # invisible chars, Tags block, homoglyphs
│ │ ├── paraphraser.py # pluggable rewrite backends
│ │ └── kgw_detector.py # detector + Viterbi embedder
│ ├── media/
│ │ ├── c2pa_remover.py # JUMBF/EXIF/XMP stripping + inspection
│ │ └── stego_breaker.py # DWT thresholding, Fourier phase, dither
│ └── audio/
│ └── audio_cleaner.py # phase randomization, notch shifting
└── evaluation/
├── metrics.py # SSIM, PSNR, PPL, semantic similarity
├── datasets.py # deterministic fixture generation
└── runner.py # measured benchmark harness
pytest -q
384 Tests. Die Testsuite deckt den SSE-Frame-Parser bei adversarialen Chunk-Grenzen ab, den Proxy-Streaming-Lebenszyklus, jeden PIL-Bildmodus, SSIM sowohl gegen einen geschlossenen Formwert als auch gegen eine Brute-Force-Referenzimplementierung und die Erkennbarkeit von Wasserzeichen im Datensatz.
Async-Tests schlagen lautstark fehl, wenn kein Async-Plugin installiert ist, anstatt stillschweigend übersprungen zu werden.
---
## 📖 Zitieren und Weiterverwendung
Lizenziert unter Apache-2.0, was Nutzung, Modifikation und Weiterverbreitung gestattet, sofern der Urheberrechtshinweis und die Namensnennung von **Muhammad Rakibul Islam** erhalten bleiben. Siehe [LICENSE](https://github.com/rakib-nyc/nullorigin/blob/HEAD/LICENSE) und [NOTICE](https://github.com/rakib-nyc/nullorigin/blob/HEAD/NOTICE).
Wenn diese Arbeit eine Veröffentlichung unterstützt, zitieren Sie sie bitte wie folgt:```bibtex
@software{islam_nullorigin_2026,
author = {Islam, Muhammad Rakibul},
title = {{NullOrigin}: Universal AI Provenance and Watermark
Sanitization Middleware},
year = {2026},
version = {1.2.0},
url = {https://github.com/rakib-nyc/nullorigin},
note = {Research artifact for watermarking robustness evaluation}
}
Dieses Repository wird als fertiges Forschungsartefakt veröffentlicht und akzeptiert keine Pull-Requests. Du kannst es unter den Bedingungen der Lizenz gerne forken. Fragen und Erkenntnisse sind per E-Mail an [email protected] willkommen.
Version 1.0.0. Die Suite ist entsprechend ihrer Spezifikation vollständig und umfassend getestet, mit diesen bekannten Einschränkungen:
[nli] sind Rollenwechsel, bei denen jede Entität erhalten bleibt, nicht erkennbar, und der Bericht sagt dies aus.Siehe CHANGELOG.md für die Versionshistorie.
Dieses Repository ist ein forschungstaugliches Artefakt, das für statistische Forschung, Datenschutzbewertung, Robustheits-Benchmarking von Wasserzeichen und kryptografische Resilienztests veröffentlicht wurde.```text Copyright 2026 Muhammad Rakibul Islam [email protected]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
**KEINERLEI GEWÄHRLEISTUNG.** DIE SOFTWARE WIRD „WIE BESEHEN" BEREITGESTELLT, OHNE JEGLICHE GEWÄHRLEISTUNG,
AUSDRÜCKLICH ODER STILLSCHWEIGEND.
| Schicht | Was es tatsächlich tut |
|---|
| Unsichtbare Zeichen | Voll wirksam. Zero-Width-, Bidi-Steuer-, Varianten-Selektor- und Unicode-Tags-Block-Payloads werden vollständig entfernt, mit einem gemeldeten Zählerstand. Querschriftliche Homoglyph-Verwechslungen (Kyrillisch/Griechisch, die als ASCII erscheinen) werden gefaltet. |
| Dokumentmetadaten (.docx) | Voll wirksam. Autor, letzter Bearbeiter, Revisionszähler, Zeitstempel, Vorlage und Anwendungsversion werden aus docProps entfernt, wobei die Formatierung bytegenau erhalten bleibt. |
| C2PA / EXIF / XMP | Voll wirksam. Das Bild wird aus rohen Pixelproben in einen frischen Container neu aufgebaut, sodass signierte JUMBF-Manifeste und alle Metadaten verschwinden. Durch Tests gegen getaggte Fixtures verifiziert. |
| KGW-statistisches Wasserzeichen | Hängt vollständig vom Rewrite-Backend ab. Ohne konfiguriertes lokales Modell überlebt das statistische Wasserzeichen — das Tool sagt dies deutlich, statt etwas anderes zu suggerieren. |
| SynthID-Text / SynthID-Image / Tree-Ring | Hier nicht verifizierbar. Diese verwenden private Schlüssel und proprietäre Dekoder. NullOrigin wendet die in der Literatur beschriebenen Störungen an, aber es wird keine Behauptung aufgestellt, dass sie die echten Detektoren überwinden, weil es keinen öffentlichen Detektor gibt, gegen den gemessen werden könnte. |
| AudioSeal / SynthID-Audio | Hier nicht verifizierbar, aus demselben Grund. |
| Optional | Docker 20.10+ mit Compose v2 |
| Fall | lexical_f1 | Embedding-Cosinus | bidirektionales NLI |
|---|
| Negation entfernt | 0.70 | 0.77 ✓ | 0.000 ✓ |
| Zahl 5 → 50 | 0.82 | 0.81 ✓ | 0.000 ✓ |
| Entitäts-/Rollenvertauschung | 0.81 | 0.985 ✗ | 0.000 ✓ |
| Quantor alle → einige | 0.88 | 0.91 ✓ | 0.000 ✓ |
| Absicherung entfernt | 0.27 | 0.953 ✗ | 0.011 ✓ |
| "must not" → "must" | 0.83 | 0.947 ✗ | 0.000 ✓ |
| originalgetreue Umschreibung | 0.33 | 0.931 ✓ | 0.998 ✓ |