Zum Inhalt springen

VF-1064: Signed Web Pages

Status: Draft
Autor: Steven Noack
Erstellt: 20. Dezember 2025
Kind: 1064 (regular) / 30064 (replaceable)


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.


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

KindTypUse Case
1064RegularJede Version bleibt erhalten (History)
30064ReplaceableNur aktuelle Version zählt
{
"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": "..."
}
TagBeschreibungBeispiel
urlCanonical URL der signierten Seitehttps://example.com/page
xSHA-256 Hash des Inhaltsa1b2c3d4... (64 Zeichen)
mMIME Typetext/html
TagBeschreibungBeispiel
dIdentifier für replaceable Eventsexample.com/page
titleSeitentitelVision & Roadmap
selectorCSS-Selector für signierten Bereichmain, article, #content
snapshotISO-Timestamp der Hash-Berechnung2025-12-20T10:30:00Z
previousEvent-ID der vorherigen Versionabc123...
authorAutorennameSteven Noack
langSprachede, en
TagBeschreibung
signed_bySignatur-Service
signed_by_urlURL des Services

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.

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 – Hauptinhalt
  • article – Artikel-Content
  • #content – Spezifischer Container
  • .prose – Content-Bereich

Vor dem Hashen wird der Content normalisiert:

  1. Whitespace komprimieren
  2. Leere Tags entfernen
  3. Attribute sortieren
  4. Self-closing Tags vereinheitlichen
function normalize(html) {
return html
.replace(/\s+/g, ' ') // Whitespace
.replace(/>\s+</g, '><') // Zwischen Tags
.trim();
}

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>

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 ❌ Fehlgeschlagen

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

Nur die neueste Version zählt:

Event mit ["d", "example.com/page"]
→ Neues Event mit gleichem d-Tag ersetzt das alte

Vorteile: Weniger Noise, klare “aktuelle Wahrheit”


LLMs können signierte Seiten höher gewichten:

Unsignierte Quelle: Trust 0.5
Signierte Quelle: Trust 0.8
Signiert + bekannter Autor: Trust 0.95

Nachweis wer was wann publiziert hat:

“Diese Seite wurde am 20.12.2025 von npub1… signiert.”

Originalautor kann Erstveröffentlichung beweisen:

“Mein signiertes Event ist älter als deren Kopie.”

Signierte Snapshots für Wayback-Style Archive:

snapshot-2025-12-20.vf1064-abc123.html
snapshot-2025-12-21.vf1064-def456.html

VisionFusen nutzt folgendes Schema:

{name}.vf{kind}-{hash8}.{ext}

Beispiele:

vision-page.vf1064-3f7a2c1d.html
about.vf30064-8e9f0a1b.html
blog-post.vf1064-c2d3e4f5.html

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;
}
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),
};
}

  1. Dynamische Seiten: Wie umgehen mit Server-Rendered Content der sich ändert?
  2. SPAs: Wie hasht man eine React/Vue App?
  3. Canonical URL: Was wenn eine Seite mehrere URLs hat?
  4. Robots/Crawling: Sollte es ein nostr-verification.json geben (wie robots.txt)?

MethodeDezentralVerifizierbarTimestamp
SSL/TLS❌ CA-basiert❌ Nur Transport
Meta Author Tag❌ Nicht signiert
Blockchain Notary✅ Teuer
VF-1064✅ Gratis


VisionFusen Spezifikation – Dezember 2025

Signiert · 3fd7dbb2... · CC BY 4.0 · 21.12.2025