374 lines
12 KiB
HTML
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>
|