Files
nostr_quantum_preparation/www/verify.html
T

374 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; connect-src wss: https:; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none';" />
<meta name="referrer" content="no-referrer" />
<title>Verify NIP-QR Event</title>
<link rel="stylesheet" href="./css/client.css" />
<link rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;," />
<style>
#divHeaderText {
font-size: calc(max(var(--header-height), var(--header-min-height)) * 0.5) !important;
}
#divBody {
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: center !important;
justify-content: flex-start !important;
align-content: flex-start !important;
}
.pq-container {
max-width: 760px;
width: 100%;
padding: 10px;
}
.pq-card {
background: var(--secondary-color);
border: var(--border);
border-radius: var(--border-radius);
padding: 25px;
margin-bottom: 20px;
}
.pq-card-title {
font-size: 18px;
font-weight: bold;
color: var(--primary-color);
margin-bottom: 15px;
}
.pq-info-text {
font-size: 14px;
color: var(--primary-color);
line-height: 1.6;
margin-bottom: 15px;
}
.pq-info-text strong { color: var(--primary-color); }
.pq-button {
background: var(--primary-color);
color: var(--secondary-color);
border: var(--border-width) solid var(--primary-color);
border-radius: var(--border-radius);
padding: 12px 24px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: opacity 0.2s;
width: 100%;
}
.pq-button:hover { opacity: 0.7; }
.pq-button:disabled { opacity: 0.3; cursor: not-allowed; }
.pq-button-row {
display: flex;
gap: 10px;
margin-top: 10px;
}
.pq-button-row .pq-button { width: auto; }
.pq-input {
width: 100%;
background: var(--secondary-color);
border: var(--border);
border-radius: var(--border-radius);
padding: 12px;
font-size: 14px;
color: var(--primary-color);
margin: 10px 0;
box-sizing: border-box;
}
.pq-input:focus {
outline: none;
border-color: var(--accent-color);
}
.pq-textarea {
width: 100%;
background: var(--secondary-color);
border: var(--border);
border-radius: var(--border-radius);
padding: 12px;
font-size: 12px;
color: var(--primary-color);
margin: 10px 0;
min-height: 160px;
resize: vertical;
font-family: monospace;
box-sizing: border-box;
}
.pq-textarea:focus {
outline: none;
border-color: var(--accent-color);
}
.pq-status {
padding: 10px 15px;
border-radius: var(--border-radius);
margin: 10px 0;
font-size: 14px;
text-align: left;
}
.pq-status-info {
background: var(--secondary-color);
border: var(--border);
}
.pq-status-success {
background: var(--accent-color);
color: var(--secondary-color);
}
.pq-status-error {
background: var(--accent-color);
color: var(--secondary-color);
}
.pq-result-list {
display: flex;
flex-direction: column;
gap: 8px;
margin: 15px 0;
}
.pq-result-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 15px;
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border);
border-radius: var(--border-radius);
font-size: 14px;
}
.pq-result-valid {
border-left: 4px solid #00aa00;
}
.pq-result-invalid {
border-left: 4px solid #cc0000;
}
.pq-event-preview {
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border);
border-radius: var(--border-radius);
padding: 15px;
font-size: 11px;
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow-y: auto;
text-align: left;
margin-top: 10px;
}
.pq-link {
color: var(--accent-color);
text-decoration: underline;
cursor: pointer;
}
.pq-tabs {
display: flex;
gap: 4px;
margin-bottom: 15px;
border-bottom: var(--border);
}
.pq-tab {
padding: 10px 20px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
color: var(--muted-color);
border-bottom: 3px solid transparent;
transition: color 0.2s, border-color 0.2s;
}
.pq-tab:hover { color: var(--primary-color); }
.pq-tab.pq-tab-active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
.pq-tab-panel { display: none; }
.pq-tab-panel.pq-tab-panel-active { display: block; }
.pq-ots-badge {
display: inline-block;
padding: 3px 10px;
border-radius: var(--border-radius);
font-size: 12px;
font-weight: bold;
margin-left: 8px;
}
.pq-ots-badge-pending {
background: #f0ad4e;
color: #fff;
}
.pq-ots-badge-confirmed {
background: #00aa00;
color: #fff;
}
.pq-ots-badge-none {
background: var(--muted-color);
color: var(--secondary-color);
}
</style>
</head>
<body>
<!-- HEADER -->
<div id="divHeader">
<div id="divHeaderFlexLeft"></div>
<div id="divHeaderFlexCenter">
<div class="divHeaderText" id="divHeaderText">Verify NIP-QR Event</div>
</div>
<div id="divHeaderFlexRight"></div>
</div>
<!-- BODY -->
<div id="divBody">
<div class="pq-container">
<div class="pq-card">
<div class="pq-card-title">Verify NIP-QR Migration Event</div>
<div class="pq-info-text" style="color: var(--accent-color); font-size: 13px;">
<strong>Research prototype.</strong> Verification confirms key linkage and signature
validity; it does not mean the identity is post-quantum secure. Bitcoin OTS confirmation
is API-assisted (trusts a public explorer), not a full light-client verification.
</div>
<div class="pq-info-text">
Verify post-quantum migration events (kind 9999) by querying relays for a user's pubkey,
or by pasting event JSON directly. The kind 9999 event wraps a kind 1 announcement
(embedded in its content as JSON) and carries an OpenTimestamps proof. Verification checks,
reported as separate result lines:
the kind 9999 secp256k1 signature, the embedded kind 1 event's secp256k1 signature,
the e tag (kind 1 event ID), the sha256 tag (full kind 1 event hash), each PQ signature
individually (ML-DSA-44, ML-DSA-65, SLH-DSA-128s, Falcon-512; ML-KEM-768 has no signature),
the PQ algorithm policy (all mandatory algorithms present and verified), identity binding
(outer/embedded/expected author match), and the OpenTimestamps proof.
</div>
<div class="pq-info-text">
<a href="./" class="pq-link">Back to migration page</a>
</div>
<div class="pq-tabs">
<div class="pq-tab pq-tab-active" id="pqTabRelay">Query Relay</div>
<div class="pq-tab" id="pqTabPaste">Paste Event JSON</div>
</div>
<!-- TAB 1: Query Relay -->
<div class="pq-tab-panel pq-tab-panel-active" id="pqPanelRelay">
<div class="pq-info-text">
Enter a Nostr pubkey (hex or npub) and one or more relay URLs. The page will query the
relay(s) for the latest kind 9999 event from that pubkey and verify it.
</div>
<input type="text" class="pq-input" id="pqPubkeyInput"
placeholder="Pubkey (hex or npub), e.g. 3bf0c63f869a8972... or npub1..." />
<input type="text" class="pq-input" id="pqRelayInput"
value="wss://laantungir.net/relay,wss://relay.damus.io,wss://nos.lol"
placeholder="wss://relay1.com,wss://relay2.com" />
<button class="pq-button" id="pqQueryBtn">Query & Verify</button>
<div id="pqQueryStatus"></div>
</div>
<!-- TAB 2: Paste Event JSON -->
<div class="pq-tab-panel" id="pqPanelPaste">
<div class="pq-info-text">
Paste a kind 9999 event JSON below to verify all signatures.
</div>
<textarea class="pq-textarea" id="pqEventInput" placeholder='Paste kind 9999 event JSON here, e.g.:
{
"id": "...",
"pubkey": "...",
"kind": 9999,
"content": "{\"id\":\"...\",\"pubkey\":\"...\",\"kind\":1,\"content\":\"I am signaling...\",\"tags\":[...],\"sig\":\"...\"}",
"tags": [
["e", "<kind 1 event id>"],
["sha256", "<hash of full kind 1 event>"],
["ots", "<base64 .ots proof>"]
],
"sig": "..."
}'></textarea>
<button class="pq-button" id="pqVerifyBtn">Verify Signatures</button>
<div id="pqVerifyStatus"></div>
<div class="pq-info-text" style="margin-top: 12px; font-size: 13px;">
Or import a proof archive file (offline verification without a relay):
</div>
<input type="file" id="pqArchiveFileInput" accept="application/json,.json" style="margin-top: 6px; font-size: 13px;" />
</div>
<!-- Shared results area -->
<div id="pqResultsWrap" style="display: none;">
<div class="pq-info-text" style="margin-top: 15px; margin-bottom: 5px;">
<strong>Verification Results:</strong>
<span id="pqOtsBadge"></span>
</div>
<div class="pq-result-list" id="pqResultList"></div>
</div>
<div id="pqEventJsonWrap" style="display: none; margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;"><strong>Full event JSON:</strong></div>
<div class="pq-event-preview" id="pqEventJson"></div>
<div class="pq-button-row" style="margin-top: 10px;">
<button class="pq-button pq-button-secondary" id="pqDownloadEventBtn">Download Event JSON</button>
</div>
</div>
<!-- OTS upgrade section -->
<div id="pqOtsUpgradeWrap" style="display: none; margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;"><strong>OpenTimestamps:</strong></div>
<div id="pqOtsInfo" class="pq-status pq-status-info"></div>
<div class="pq-button-row">
<button class="pq-button" id="pqOtsUpgradeBtn">Upgrade OTS Proof</button>
<button class="pq-button" id="pqOtsRepublishBtn" style="display:none;">Publish Upgraded Event</button>
</div>
<div id="pqOtsUpgradeStatus"></div>
</div>
</div>
</div>
</div>
<!-- FOOTER -->
<div id="divFooter">
<div id="divFooterLeft" class="divFooterBox"></div>
<div id="divFooterCenter" class="divFooterBox"></div>
<div id="divFooterRight" class="divFooterBox"></div>
</div>
<!-- SCRIPTS -->
<script src="/nostr-login-lite/nostr.bundle.js" integrity="sha384-LNnPDD++DaWxljIhMLmfaoEoKB0B1HmACC6gNGLG+Qnmosprf/AX7u84pVY8xMpM" crossorigin="anonymous"></script>
<script src="/nostr-login-lite/nostr-lite.js" integrity="sha384-IQwa65eDC5trGjKn4cuEazmFiHTHD65gIqsjpzDtouc+j0MlyI927SZgHMWSi2aC" crossorigin="anonymous"></script>
<!-- Version display (G56-08: extracted to external file, self-initializing) -->
<script src="./js/version-display.js"></script>
<!-- Main application module (G56-08: extracted to external file for CSP compliance) -->
<script type="module" src="./js/verify-app.mjs"></script>
</body>
</html>