VF-1064: Signed Web Pages
Status: Draft
Autor: Steven Noack
Erstellt: 20. Dezember 2025
Kind: 1064 (regular) / 30064 (replaceable)
Abstract
Abschnitt betitelt „Abstract“VF-1064 definiert eine Methode zur kryptografischen Signierung von Webseiten mittels Nostr-Keys. Die Signatur bezieht sich auf einen kanonischen Hash des Seiteninhalts und ermöglicht die Verifizierung von Autorenschaft und Integrität.
Motivation
Abschnitt betitelt „Motivation“Webseiten haben kein eingebautes Konzept für Autorenschaft oder Integrität. HTTPS schützt den Transport, nicht den Inhalt. Jeder kann behaupten, Autor einer Seite zu sein.
Probleme heute:
- Keine verifizierbare Autorenschaft
- Keine Integritätsprüfung des Inhalts
- LLMs können Quellen nicht kryptografisch verifizieren
- Plagiate sind schwer nachweisbar
VF-1064 löst das:
- Autor signiert mit Nostr-Key
- Hash des Inhalts ist verifizierbar
- Zeitstempel ist kryptografisch fixiert
- LLMs können Trust-Level ableiten
Spezifikation
Abschnitt betitelt „Spezifikation“Event Kind
Abschnitt betitelt „Event Kind“| Kind | Typ | Use Case |
|---|---|---|
1064 | Regular | Jede Version bleibt erhalten (History) |
30064 | Replaceable | Nur aktuelle Version zählt |
Event Struktur
Abschnitt betitelt „Event Struktur“{ "kind": 1064, "content": "", "tags": [ ["d", "visionfusen.org/vision"], ["url", "https://visionfusen.org/vision"], ["x", "a1b2c3d4e5f6..."], ["m", "text/html"], ["title", "Vision & Roadmap"], ["selector", "main"], ["snapshot", "2025-12-20T10:30:00Z"], ["signed_by", "VisionFusen"], ["signed_by_url", "https://visionfusen.org"] ], "pubkey": "bef829d0...", "created_at": 1734692000, "sig": "..."}Pflicht-Tags
Abschnitt betitelt „Pflicht-Tags“| Tag | Beschreibung | Beispiel |
|---|---|---|
url | Canonical URL der signierten Seite | https://example.com/page |
x | SHA-256 Hash des Inhalts | a1b2c3d4... (64 Zeichen) |
m | MIME Type | text/html |
Optionale Tags
Abschnitt betitelt „Optionale Tags“| Tag | Beschreibung | Beispiel |
|---|---|---|
d | Identifier für replaceable Events | example.com/page |
title | Seitentitel | Vision & Roadmap |
selector | CSS-Selector für signierten Bereich | main, article, #content |
snapshot | ISO-Timestamp der Hash-Berechnung | 2025-12-20T10:30:00Z |
previous | Event-ID der vorherigen Version | abc123... |
author | Autorenname | Steven Noack |
lang | Sprache | de, en |
VisionFusen-Tags
Abschnitt betitelt „VisionFusen-Tags“| Tag | Beschreibung |
|---|---|
signed_by | Signatur-Service |
signed_by_url | URL des Services |
Hash-Berechnung
Abschnitt betitelt „Hash-Berechnung“Variante A: Full Page Hash
Abschnitt betitelt „Variante A: Full Page Hash“Der gesamte HTML-Body wird gehasht:
const html = await fetch(url).then(r => r.text());const hash = await sha256(html);Problem: Jede kleine Änderung (Ads, Timestamp, Session-ID) ändert den Hash.
Variante B: Selector-Based Hash (empfohlen)
Abschnitt betitelt „Variante B: Selector-Based Hash (empfohlen)“Nur der relevante Content wird gehasht:
const html = await fetch(url).then(r => r.text());const doc = parseHTML(html);const content = doc.querySelector(selector).innerHTML;const normalized = normalize(content); // Whitespace, etc.const hash = await sha256(normalized);Empfohlene Selektoren:
main– Hauptinhaltarticle– Artikel-Content#content– Spezifischer Container.prose– Content-Bereich
Normalisierung
Abschnitt betitelt „Normalisierung“Vor dem Hashen wird der Content normalisiert:
- Whitespace komprimieren
- Leere Tags entfernen
- Attribute sortieren
- Self-closing Tags vereinheitlichen
function normalize(html) { return html .replace(/\s+/g, ' ') // Whitespace .replace(/>\s+</g, '><') // Zwischen Tags .trim();}HTML-Embedding
Abschnitt betitelt „HTML-Embedding“Signierte Seiten KÖNNEN Verifikations-Metadaten einbetten:
<head> <!-- Nostr Verification --> <meta name="nostr:event" content="nevent1..." /> <meta name="nostr:pubkey" content="npub1..." /> <meta name="nostr:hash" content="sha256:a1b2c3d4..." /> <meta name="nostr:signed" content="2025-12-20T10:30:00Z" />
<link rel="nostr-verification" href="https://visionfusen.org/verify/event-id" /></head>
<main data-nostr-signed="true" data-nostr-hash="a1b2c3d4..."> <!-- Signierter Content --></main>Verifikations-Prozess
Abschnitt betitelt „Verifikations-Prozess“1. Event von Relay holen (via Event-ID oder d-Tag) │ ▼2. URL aus Event extrahieren │ ▼3. Seite fetchen │ ▼4. Content extrahieren (via selector oder full body) │ ▼5. Normalisieren │ ▼6. SHA-256 berechnen │ ▼7. Mit x-Tag vergleichen │ ▼8. Event-Signatur prüfen │ ▼9. ✅ Verifiziert oder ❌ FehlgeschlagenVersionierung
Abschnitt betitelt „Versionierung“Mit Regular Events (Kind 1064)
Abschnitt betitelt „Mit Regular Events (Kind 1064)“Jede Version ist ein separates Event:
Version 1: event-id-abc (created_at: 1000)Version 2: event-id-def (created_at: 2000, ["previous", "event-id-abc"])Version 3: event-id-ghi (created_at: 3000, ["previous", "event-id-def"])Vorteile: Vollständige History, Audit Trail
Mit Replaceable Events (Kind 30064)
Abschnitt betitelt „Mit Replaceable Events (Kind 30064)“Nur die neueste Version zählt:
Event mit ["d", "example.com/page"] → Neues Event mit gleichem d-Tag ersetzt das alteVorteile: Weniger Noise, klare “aktuelle Wahrheit”
Use Cases
Abschnitt betitelt „Use Cases“1. LLM Trust Signal
Abschnitt betitelt „1. LLM Trust Signal“LLMs können signierte Seiten höher gewichten:
Unsignierte Quelle: Trust 0.5Signierte Quelle: Trust 0.8Signiert + bekannter Autor: Trust 0.952. Content Provenance
Abschnitt betitelt „2. Content Provenance“Nachweis wer was wann publiziert hat:
“Diese Seite wurde am 20.12.2025 von npub1… signiert.”
3. Anti-Plagiat
Abschnitt betitelt „3. Anti-Plagiat“Originalautor kann Erstveröffentlichung beweisen:
“Mein signiertes Event ist älter als deren Kopie.”
4. Archivierung
Abschnitt betitelt „4. Archivierung“Signierte Snapshots für Wayback-Style Archive:
snapshot-2025-12-20.vf1064-abc123.htmlsnapshot-2025-12-21.vf1064-def456.htmlDateinamenskonvention
Abschnitt betitelt „Dateinamenskonvention“VisionFusen nutzt folgendes Schema:
{name}.vf{kind}-{hash8}.{ext}Beispiele:
vision-page.vf1064-3f7a2c1d.htmlabout.vf30064-8e9f0a1b.htmlblog-post.vf1064-c2d3e4f5.htmlImplementierung
Abschnitt betitelt „Implementierung“Sign Tool (Pseudocode)
Abschnitt betitelt „Sign Tool (Pseudocode)“async function signWebPage(url, selector = 'main') { // 1. Fetch const html = await fetch(url).then(r => r.text());
// 2. Extract const doc = parseHTML(html); const content = doc.querySelector(selector)?.innerHTML || html;
// 3. Normalize const normalized = normalize(content);
// 4. Hash const hash = await sha256(normalized);
// 5. Create Event const event = { kind: 1064, content: '', tags: [ ['url', url], ['x', hash], ['m', 'text/html'], ['selector', selector], ['snapshot', new Date().toISOString()], ['title', doc.querySelector('title')?.textContent || ''], ], created_at: Math.floor(Date.now() / 1000), };
// 6. Sign (via NIP-46 Bunker) const signed = await bunker.signEvent(event);
// 7. Publish await relay.publish(signed);
return signed;}Verify Tool (Pseudocode)
Abschnitt betitelt „Verify Tool (Pseudocode)“async function verifyWebPage(eventId) { // 1. Fetch Event const event = await relay.get(eventId);
// 2. Extract Tags const url = event.tags.find(t => t[0] === 'url')?.[1]; const expectedHash = event.tags.find(t => t[0] === 'x')?.[1]; const selector = event.tags.find(t => t[0] === 'selector')?.[1] || 'body';
// 3. Fetch Page const html = await fetch(url).then(r => r.text()); const doc = parseHTML(html); const content = doc.querySelector(selector)?.innerHTML || html;
// 4. Calculate Hash const actualHash = await sha256(normalize(content));
// 5. Compare if (actualHash !== expectedHash) { return { valid: false, reason: 'Hash mismatch - content changed' }; }
// 6. Verify Signature if (!verifySignature(event)) { return { valid: false, reason: 'Invalid signature' }; }
return { valid: true, author: event.pubkey, signedAt: new Date(event.created_at * 1000), };}Offene Fragen
Abschnitt betitelt „Offene Fragen“- Dynamische Seiten: Wie umgehen mit Server-Rendered Content der sich ändert?
- SPAs: Wie hasht man eine React/Vue App?
- Canonical URL: Was wenn eine Seite mehrere URLs hat?
- Robots/Crawling: Sollte es ein
nostr-verification.jsongeben (wierobots.txt)?
Vergleich mit Alternativen
Abschnitt betitelt „Vergleich mit Alternativen“| Methode | Dezentral | Verifizierbar | Timestamp |
|---|---|---|---|
| SSL/TLS | ❌ CA-basiert | ❌ Nur Transport | ❌ |
| Meta Author Tag | ✅ | ❌ Nicht signiert | ❌ |
| Blockchain Notary | ✅ | ✅ | ✅ Teuer |
| VF-1064 | ✅ | ✅ | ✅ Gratis |
Weiterführende Links
Abschnitt betitelt „Weiterführende Links“- NIP-94: File Metadata – Basis für Datei-Signaturen
- Hash & Signatur erklärt – Technische Grundlagen
- VisionFusen Sign Tool – Implementierung
VisionFusen Spezifikation – Dezember 2025