Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e61c6a5a8 | ||
|
|
399a633697 | ||
|
|
a662686a0b | ||
|
|
668fb65efb | ||
|
|
270cddc433 | ||
|
|
33e06adc4a | ||
|
|
b46d081211 | ||
|
|
57d8d2fcf4 | ||
|
|
79185f9909 | ||
|
|
94f03f2999 |
@@ -1,5 +1,14 @@
|
||||
---
|
||||
slug: nostr-quantum-preparation
|
||||
title: Nostr Quantum Preparation README.md
|
||||
summary: This project is an interim solution to the problem for those who are concerned about keeping their identities secure through the transition, and also to give developers breathing room.
|
||||
image: https://blossom.laantungir.net/5b96cc0c5ba4a8081e5ee8e5a2e718f28c0cbc77f218c55d956b9be57c120ef1.jpg
|
||||
tags: [nostr, quantum, readme.md]
|
||||
---
|
||||
|
||||
# Nostr Quantum Preparation
|
||||
|
||||
/tmp/sb_test_save.jpg
|
||||
|
||||
## TLDR
|
||||
|
||||
@@ -23,7 +32,7 @@ I have currently used the following post quantum algorithms:
|
||||
|
||||
This app will generate public-private keypairs for each of these, publish on nostr your public key for each, and sign it with your current Nostr identity via your favorite signer (the nsec never leaves the signer).
|
||||
|
||||
You are making a statement to the world: "Here are post quantum pubkeys. If quantum computers hit, you know that the person signing using these can only be me."
|
||||
You are making a statement to the world: "Here are post quantum pubkeys. If quantum computers hit, you know that the person using these can only be me."
|
||||
|
||||
This entire kind 1 event is then hashed and stamped into a block on Bitcoin using OpenTimestamps.
|
||||
|
||||
@@ -204,12 +213,18 @@ Bitcoin's proof-of-work chain is quantum-resistant not because Bitcoin's signatu
|
||||
|
||||
### Current OTS verification status
|
||||
|
||||
> **Trust model.** The current OTS verification is **API-assisted**, not a full Bitcoin light-client
|
||||
> verification. Treat results accordingly.
|
||||
> **Trust model.** The current OTS verification is **multi-explorer-checked**, not a full Bitcoin
|
||||
> light-client verification. Treat results accordingly.
|
||||
|
||||
The current implementation parses the `.ots` proof, validates the Merkle path against a block header from a public Bitcoin explorer API (mempool.space), and detects confirmation by searching the proof bytes for the block-header attestation magic. Proof *upgrading* (asking calendars for a confirmed proof) is delegated to a server-side helper.
|
||||
The current implementation parses the `.ots` proof, walks the full Merkle path, and validates the resulting Merkle root against a Bitcoin block header fetched from **two independent public explorer APIs** (blockstream.info and mempool.space). The two providers are cross-checked and the verifier fails closed on disagreement, making coordinated false responses harder. The result's `trustMode` field reports the achieved trust level:
|
||||
|
||||
**Not yet done:** full light-client verification (validating block headers, proof-of-work, difficulty, and chain linkage independently, without trusting an explorer API) and multi-explorer cross-checking. Because the block header is trusted from a single API, a compromised explorer could falsify a confirmation — the UI labels this as "API-checked," not "cryptographically verified on Bitcoin." The vendored `javascript-opentimestamps` library in `resources/` provides the primitives for full light-client verification, which is planned for a future release.
|
||||
- `multi-explorer-checked` — both providers agreed on the Merkle root (strongest available mode).
|
||||
- `single-explorer-checked` — only one provider responded (weaker; the UI labels it accordingly).
|
||||
- `structural-only` — no Bitcoin attestation was verified (pending or none).
|
||||
|
||||
Proof *upgrading* (asking calendars for a confirmed proof) is delegated to a server-side helper.
|
||||
|
||||
**Not yet done:** full light-client verification — validating block headers, proof-of-work, difficulty, and chain linkage independently, without trusting any explorer API. Because the block header is trusted from explorer APIs (even when cross-checked), a compromised/colluding set of explorers could still falsify a confirmation. The UI labels results as "multi-explorer-checked (cross-verified)" or "single-explorer-checked (trusted API)," never as "trustless." The vendored `javascript-opentimestamps` library in `resources/` provides the primitives for full light-client verification, which is planned for a future release.
|
||||
|
||||
---
|
||||
|
||||
@@ -288,20 +303,25 @@ The current implementation is a static web app (`www/`) that performs the full p
|
||||
| NIP-01 event ID computation and Schnorr verification | Implemented | `www/js/pq-crypto.mjs` |
|
||||
| OpenTimestamps submission (pending proof) | Implemented | `www/js/pq-crypto.mjs` |
|
||||
| OTS upgrade polling (via server helper) | Implemented | `www/js/pq-crypto.mjs` |
|
||||
| OTS full Merkle-path verification, multi-explorer cross-checked (blockstream + mempool) | Implemented | `www/js/pq-crypto.mjs` |
|
||||
| Canonical proof carrier selection (earliest valid Bitcoin anchor wins) | Implemented | `www/js/pq-crypto.mjs` |
|
||||
| Signer output validation (G56-05: reject mutated/invalid signer responses) | Implemented | `www/js/pq-crypto.mjs` |
|
||||
| Proof archive export/import (offline verification without a relay) | Implemented | `www/js/pq-crypto.mjs`, `www/verify.html` |
|
||||
| Versioned PQ algorithm policy (F-D3: mandatory/KEM/ignored sets) | Implemented | `www/js/nip-qr-policy.mjs` |
|
||||
| Relay publishing (WebSocket) | Implemented | `www/index.html` |
|
||||
| Verification page (query relay or paste event JSON) | Implemented | `www/verify.html` |
|
||||
| Verification page (query relay, paste event JSON, or import proof archive) | Implemented | `www/verify.html` |
|
||||
| NIP-07 signer integration (nostr-login-lite) | Implemented | `www/index.html` |
|
||||
| Test suite + deterministic test vectors (seed → pubkeys) | Implemented | `test/pq-crypto.test.mjs`, `test/vectors/` |
|
||||
|
||||
### What is not yet implemented
|
||||
|
||||
| Component | Description |
|
||||
|---|---|
|
||||
| Full light-client Bitcoin verification | Validate block headers, proof-of-work, difficulty, and chain linkage independently of an explorer API (the vendored `javascript-opentimestamps` library in `resources/` provides primitives). Current path trusts a single explorer API for the header. |
|
||||
| Multi-explorer cross-checking | Compare independent Bitcoin APIs and reject disagreement |
|
||||
| Full light-client Bitcoin verification | Validate block headers, proof-of-work, difficulty, and chain linkage independently of any explorer API (the vendored `javascript-opentimestamps` library in `resources/` provides primitives). The current path cross-checks two explorer APIs but still trusts them for the header rather than verifying PoW/difficulty/chain-linkage itself. |
|
||||
| Raw-nsec preparation (Component 4) | Encrypt old nsec, publish kind 30078, 36-word phrase encoding |
|
||||
| Quantum-safe self-storage (Component 5) | OTP or symmetric-key encryption for kind 30078 data |
|
||||
| PQ event authentication / rotation / revocation | Companion protocols for signing future events with PQ keys, rotating/revoking keys, PQ encryption |
|
||||
| Independent implementation / test vectors | A second implementation reproducing canonical encoding, derivation, and selection |
|
||||
| Independent implementation | A second, independent implementation reproducing canonical encoding, derivation, and selection (deterministic test vectors already exist in `test/vectors/`; a third-party reimplementation is the remaining step). |
|
||||
| Relay event-size interop testing | The kind 1 announcement is ~20–30 KiB; the kind 9999 proof carrier embeds it plus OTS proof data. The UI shows the event size and warns if it exceeds ~60 KiB (some relays reject large events). Testing against target relay policies and a compact binary format remain as future work. |
|
||||
| Reproducible-build verification of the deployed bundle | The deployed `www/pq-crypto.bundle.js` is a build artifact produced by `build-pq-bundle.js` from the audited source in `www/js/pq-crypto.mjs`. Users must currently trust that the deployed bundle matches the source. Reproducible-build verification (so anyone can rebuild the bundle byte-for-byte and confirm the deployed file matches) is on the roadmap. |
|
||||
|
||||
@@ -329,6 +349,7 @@ The current implementation is a static web app (`www/`) that performs the full p
|
||||
- [NIST FIPS 203: ML-KEM (Kyber)](https://csrc.nist.gov/pubs/fips/203/final)
|
||||
- [NIST FIPS 204: ML-DSA (Dilithium)](https://csrc.nist.gov/pubs/fips/204/final)
|
||||
- [NIST FIPS 205: SLH-DSA (SPHINCS+)](https://csrc.nist.gov/pubs/fips/205/final)
|
||||
- [NIST FIPS 206: Falcon (draft)](https://csrc.nist.gov/pubs/fips/206/ipd) — Falcon-512 signature scheme
|
||||
- [OpenTimestamps](https://opentimestamps.org/)
|
||||
- [Open Quantum Safe](https://openquantumsafe.org/)
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nostr_quantum_preparation",
|
||||
"version": "0.0.34",
|
||||
"version": "0.1.6",
|
||||
"description": "A migration strategy for bringing post-quantum security to Nostr without breaking the social graph, without requiring consensus on a single post-quantum algorithm, and without forcing existing users to abandon their identities.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -823,6 +823,21 @@ a.nostr-embed-preview-text:hover {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* Small, unobtrusive version badge fixed to the bottom-right of the page.
|
||||
Rendered by js/version-display.js. */
|
||||
.pq-version-badge {
|
||||
position: fixed;
|
||||
right: 8px;
|
||||
bottom: 6px;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
color: var(--muted-color);
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
font-family: var(--font-family, monospace);
|
||||
}
|
||||
|
||||
.svgHeaderButtons {
|
||||
fill: none;
|
||||
stroke: var(--button-color);
|
||||
|
||||
@@ -549,9 +549,6 @@
|
||||
<div id="pqEventIdDisplay" style="margin: 5px 0; font-size: 11px; color: var(--primary-color); word-break: break-all;"></div>
|
||||
<div class="pq-event-preview" id="pqSuccessEventPreview"></div>
|
||||
<div class="pq-info-text" id="pqEventSizeWarning" style="display: none; font-size: 13px; margin-top: 8px;"></div>
|
||||
<div class="pq-button-row" style="margin-top: 10px;">
|
||||
<button class="pq-button pq-button-secondary" id="pqCopyProofCarrierBtn">Copy Proof Carrier Event</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px;">
|
||||
<div class="pq-info-text" style="margin-bottom: 5px;"><strong>Relays to publish to:</strong></div>
|
||||
@@ -632,12 +629,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
+43
-12
@@ -16,6 +16,7 @@
|
||||
bytesToBase64,
|
||||
base64ToBytes,
|
||||
bytesToHex,
|
||||
hexToNpub,
|
||||
PQ_KEY_INFO,
|
||||
NIP_QR_KIND,
|
||||
buildUpgradedEvent,
|
||||
@@ -1050,16 +1051,6 @@
|
||||
if (e.key === 'Enter') { e.preventDefault(); document.getElementById('pqRelayAddBtn').click(); }
|
||||
});
|
||||
|
||||
// Copy Proof Carrier Event button
|
||||
document.getElementById('pqCopyProofCarrierBtn').addEventListener('click', () => {
|
||||
if (!pqEvent) return;
|
||||
navigator.clipboard.writeText(JSON.stringify(pqEvent, null, 2)).then(() => {
|
||||
const btn = document.getElementById('pqCopyProofCarrierBtn');
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => { btn.textContent = 'Copy Proof Carrier Event'; }, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
// Sign out button
|
||||
const pqSignOutBtn = document.getElementById('pqSignOutBtn');
|
||||
pqSignOutBtn.addEventListener('click', async () => {
|
||||
@@ -1318,7 +1309,7 @@
|
||||
const saved = loadPendingOts();
|
||||
if (!saved || !saved.confirmedPublished) {
|
||||
setStatus(document.getElementById('pqOtsStatus'), 'success', 'Bitcoin attestation verified. Publishing new kind 9999 with confirmed proof...');
|
||||
await publishUpgradedEvent(pendingOtsBytes);
|
||||
await publishUpgradedEvent(pendingOtsBytes, att ? att.height : null, att ? att.time : null);
|
||||
}
|
||||
} else if (isOtsConfirmed(pendingOtsBytes)) {
|
||||
// Structural check found a Bitcoin attestation tag but full
|
||||
@@ -1350,7 +1341,7 @@
|
||||
otsPollInterval = setInterval(poll, 60000);
|
||||
}
|
||||
|
||||
async function publishUpgradedEvent(upgradedOtsBytes) {
|
||||
async function publishUpgradedEvent(upgradedOtsBytes, bitcoinHeight = null, bitcoinTime = null) {
|
||||
const otsStatus = document.getElementById('pqOtsStatus');
|
||||
try {
|
||||
otsLog('Building upgraded proof carrier with confirmed OTS proof...');
|
||||
@@ -1385,6 +1376,46 @@
|
||||
kind1Event
|
||||
});
|
||||
|
||||
// Publish a kind 1 reply announcing the confirmed attestation.
|
||||
// This is a regular Nostr text note (reply to the original kind 1
|
||||
// announcement) so it shows up in feeds/thread views. It only needs
|
||||
// the secp256k1 signature — no PQ signatures.
|
||||
if (kind1Event && kind1Event.id) {
|
||||
try {
|
||||
otsLog('Publishing kind 1 reply announcing confirmed attestation...');
|
||||
const npub = hexToNpub(currentPubkey);
|
||||
const dateStr = bitcoinTime
|
||||
? new Date(bitcoinTime * 1000).toISOString().substring(0, 19)
|
||||
: 'unknown';
|
||||
const blockStr = bitcoinHeight ? `Bitcoin block ${bitcoinHeight} (mined ${dateStr} UTC)` : 'a Bitcoin block';
|
||||
const replyContent = `✅ The OpenTimestamps proof for my post-quantum key attestation has been confirmed in ${blockStr}. The link between my current Nostr identity and my post-quantum keys is now anchored to the Bitcoin blockchain and cannot be backdated.
|
||||
|
||||
Verify it here: https://laantungir.net/quantum-prep/verify.html?npub=${npub}
|
||||
|
||||
This is a reply to my original attestation announcement. The confirmed proof is carried in a new kind 9999 event referencing the original.`;
|
||||
const replyTemplate = {
|
||||
kind: 1,
|
||||
content: replyContent,
|
||||
tags: [
|
||||
['e', kind1Event.id, '', 'root'],
|
||||
['e', validatedEvent.id, '', 'mention']
|
||||
],
|
||||
pubkey: currentPubkey,
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
const replySigned = await window.nostr.signEvent(replyTemplate);
|
||||
const replyValidated = validateSignerOutput(replySigned, replyTemplate, currentPubkey);
|
||||
const replyResults = await publishToRelays(replyValidated, relayUrls);
|
||||
const replyOk = replyResults.filter(r => r.success).length;
|
||||
const replyFail = replyResults.filter(r => !r.success).length;
|
||||
otsLog(`Kind 1 reply published. Successful: ${replyOk}; failed: ${replyFail}.`);
|
||||
} catch (replyErr) {
|
||||
// The upgraded proof carrier is already published; a failed reply
|
||||
// is non-fatal — log it but don't fail the whole operation.
|
||||
otsLog(`WARNING: kind 1 reply failed: ${replyErr.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
setStatus(otsStatus, 'success', `Upgraded proof carrier published to ${successCount} relays. Timestamping complete.`);
|
||||
setStepDone(6);
|
||||
setStepActive(7);
|
||||
|
||||
@@ -638,7 +638,8 @@ Post-quantum public keys in tags:
|
||||
|
||||
Each post-quantum key has cryptographically signed this attestation. This event is pending timestamp on the Bitcoin blockchain via OpenTimestamps.
|
||||
|
||||
Verify this attestation: https://laantungir.net/quantum-prep/verify.html
|
||||
Verify this attestation: https://laantungir.net/quantum-prep/verify.html?npub=${npub}
|
||||
|
||||
Created at: https://laantungir.net/quantum-prep/`;
|
||||
|
||||
const statementBytes = new TextEncoder().encode(content);
|
||||
|
||||
+408
-67
@@ -22,19 +22,26 @@
|
||||
================================================================ */
|
||||
const tabRelay = document.getElementById('pqTabRelay');
|
||||
const tabPaste = document.getElementById('pqTabPaste');
|
||||
const tabSignIn = document.getElementById('pqTabSignIn');
|
||||
const panelRelay = document.getElementById('pqPanelRelay');
|
||||
const panelPaste = document.getElementById('pqPanelPaste');
|
||||
const panelSignIn = document.getElementById('pqPanelSignIn');
|
||||
|
||||
function switchTab(which) {
|
||||
const isRelay = which === 'relay';
|
||||
const isPaste = which === 'paste';
|
||||
const isSignIn = which === 'signin';
|
||||
tabRelay.classList.toggle('pq-tab-active', isRelay);
|
||||
tabPaste.classList.toggle('pq-tab-active', !isRelay);
|
||||
tabPaste.classList.toggle('pq-tab-active', isPaste);
|
||||
tabSignIn.classList.toggle('pq-tab-active', isSignIn);
|
||||
panelRelay.classList.toggle('pq-tab-panel-active', isRelay);
|
||||
panelPaste.classList.toggle('pq-tab-panel-active', !isRelay);
|
||||
panelPaste.classList.toggle('pq-tab-panel-active', isPaste);
|
||||
panelSignIn.classList.toggle('pq-tab-panel-active', isSignIn);
|
||||
}
|
||||
|
||||
tabRelay.addEventListener('click', () => switchTab('relay'));
|
||||
tabPaste.addEventListener('click', () => switchTab('paste'));
|
||||
tabSignIn.addEventListener('click', () => switchTab('signin'));
|
||||
|
||||
/* ================================================================
|
||||
SHARED DOM + STATE
|
||||
@@ -42,19 +49,56 @@
|
||||
const resultsWrap = document.getElementById('pqResultsWrap');
|
||||
const resultList = document.getElementById('pqResultList');
|
||||
const eventJsonWrap = document.getElementById('pqEventJsonWrap');
|
||||
const downloadEventBtn = document.getElementById('pqDownloadEventBtn');
|
||||
const eventJsonEl = document.getElementById('pqEventJson');
|
||||
const otsBadge = document.getElementById('pqOtsBadge');
|
||||
const otsUpgradeWrap = document.getElementById('pqOtsUpgradeWrap');
|
||||
const otsInfo = document.getElementById('pqOtsInfo');
|
||||
const otsUpgradeBtn = document.getElementById('pqOtsUpgradeBtn');
|
||||
const otsRepublishBtn = document.getElementById('pqOtsRepublishBtn');
|
||||
const otsUpgradeStatus = document.getElementById('pqOtsUpgradeStatus');
|
||||
const summaryWrap = document.getElementById('pqSummaryWrap');
|
||||
const summaryCard = document.getElementById('pqSummaryCard');
|
||||
|
||||
// OTS cards are created dynamically and appended into the shared result
|
||||
// list so they flow as part of the same card list as the signature checks.
|
||||
let otsStampCard = null;
|
||||
let otsUpgradedCard = null;
|
||||
|
||||
let currentEvent = null;
|
||||
let currentOtsBytes = null;
|
||||
let currentExpectedAuthor = null;
|
||||
|
||||
/* Braille spinner: shown inside a button while its action is running
|
||||
(e.g. Query & Verify). Advances through the standard braille spinner
|
||||
frames every 80ms, matching the spinners on the preparation page. */
|
||||
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
const buttonSpinnerIntervals = new Map();
|
||||
|
||||
function setButtonSpinner(btn) {
|
||||
if (!btn) return;
|
||||
clearButtonSpinner(btn);
|
||||
let span = btn.querySelector('.pq-btn-spinner');
|
||||
if (!span) {
|
||||
span = document.createElement('span');
|
||||
span.className = 'pq-btn-spinner';
|
||||
btn.insertBefore(span, btn.firstChild);
|
||||
}
|
||||
let frame = 0;
|
||||
span.textContent = SPINNER_FRAMES[0];
|
||||
buttonSpinnerIntervals.set(btn, setInterval(() => {
|
||||
frame = (frame + 1) % SPINNER_FRAMES.length;
|
||||
span.textContent = SPINNER_FRAMES[frame];
|
||||
}, 80));
|
||||
}
|
||||
|
||||
function clearButtonSpinner(btn) {
|
||||
if (!btn) return;
|
||||
if (buttonSpinnerIntervals.has(btn)) {
|
||||
clearInterval(buttonSpinnerIntervals.get(btn));
|
||||
buttonSpinnerIntervals.delete(btn);
|
||||
}
|
||||
const span = btn.querySelector('.pq-btn-spinner');
|
||||
if (span) span.remove();
|
||||
}
|
||||
|
||||
// F-H3: Build status DOM safely — type is internally controlled, message uses textContent
|
||||
function setStatus(element, type, message) {
|
||||
const div = document.createElement('div');
|
||||
@@ -71,13 +115,70 @@
|
||||
resultList.appendChild(item);
|
||||
}
|
||||
|
||||
// F-H3: Helper to set OTS badge (static HTML, safe) and info (textContent, safe)
|
||||
function setOtsBadge(className, text) {
|
||||
otsBadge.innerHTML = `<span class="pq-ots-badge ${className}"></span>`;
|
||||
otsBadge.querySelector('span').textContent = text;
|
||||
// OTS state tracked for the summary card.
|
||||
let otsStampState = { valid: null, label: '', detail: '' };
|
||||
let otsUpgradedState = { valid: null, label: '', detail: '' };
|
||||
|
||||
// Render an OTS card (stamp or upgraded). The card is created on demand
|
||||
// and appended to the shared result list so it flows with the signature
|
||||
// checks. `which` is 'stamp' or 'upgraded'; `state` is one of
|
||||
// 'valid' | 'invalid' | 'pending'.
|
||||
function setOtsCard(which, state, head, body) {
|
||||
const card = which === 'upgraded' ? otsUpgradedCard : otsStampCard;
|
||||
if (!card) {
|
||||
// Create the card and append it to the result list.
|
||||
const el = document.createElement('div');
|
||||
el.className = 'pq-result-item pq-result-detail';
|
||||
resultList.appendChild(el);
|
||||
if (which === 'upgraded') otsUpgradedCard = el; else otsStampCard = el;
|
||||
}
|
||||
const target = which === 'upgraded' ? otsUpgradedCard : otsStampCard;
|
||||
target.style.display = 'block';
|
||||
target.classList.remove('pq-result-valid', 'pq-result-invalid', 'pq-result-pending');
|
||||
if (state === 'valid') target.classList.add('pq-result-valid');
|
||||
else if (state === 'invalid') target.classList.add('pq-result-invalid');
|
||||
else if (state === 'pending') target.classList.add('pq-result-pending');
|
||||
// F-H3: build DOM safely — head and body use textContent
|
||||
target.innerHTML = '';
|
||||
const headEl = document.createElement('div');
|
||||
headEl.className = 'pq-result-head';
|
||||
headEl.textContent = head;
|
||||
const bodyEl = document.createElement('div');
|
||||
bodyEl.className = 'pq-result-body';
|
||||
bodyEl.textContent = body;
|
||||
target.appendChild(headEl);
|
||||
target.appendChild(bodyEl);
|
||||
}
|
||||
function setOtsInfo(text) {
|
||||
otsInfo.textContent = text;
|
||||
|
||||
function hideOtsCard(which) {
|
||||
const card = which === 'upgraded' ? otsUpgradedCard : otsStampCard;
|
||||
if (card) { card.remove(); }
|
||||
if (which === 'upgraded') otsUpgradedCard = null; else otsStampCard = null;
|
||||
}
|
||||
|
||||
// Render the final summary card based on signature validity and OTS state.
|
||||
function renderSummary(allValid) {
|
||||
const stampOk = otsStampState.valid === true;
|
||||
const upgradedOk = otsUpgradedState.valid === true;
|
||||
const anyOtsConfirmed = stampOk || upgradedOk;
|
||||
summaryWrap.style.display = 'block';
|
||||
summaryCard.classList.remove('pq-result-valid', 'pq-result-invalid', 'pq-result-pending');
|
||||
if (allValid && anyOtsConfirmed) {
|
||||
summaryCard.classList.add('pq-result-valid');
|
||||
const anchor = upgradedOk
|
||||
? (otsUpgradedState.label || 'the upgraded proof')
|
||||
: (otsStampState.label || 'the original proof');
|
||||
summaryCard.textContent = `All signatures are verified and valid, and the event is stamped on the Bitcoin blockchain (via ${anchor}).`;
|
||||
} else if (allValid && otsStampState.valid === 'pending') {
|
||||
summaryCard.classList.add('pq-result-pending');
|
||||
summaryCard.textContent = 'All signatures are verified and valid. The OpenTimestamps proof is pending — not yet stamped on the Bitcoin blockchain. You can upgrade it below.';
|
||||
} else if (allValid) {
|
||||
summaryCard.classList.add('pq-result-invalid');
|
||||
summaryCard.textContent = 'All signatures are verified and valid, but no Bitcoin timestamp attestation was found.';
|
||||
} else {
|
||||
summaryCard.classList.add('pq-result-invalid');
|
||||
summaryCard.textContent = 'Some checks failed verification. See the results above for details.';
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
@@ -168,9 +269,13 @@
|
||||
resultsWrap.style.display = 'none';
|
||||
eventJsonWrap.style.display = 'none';
|
||||
otsUpgradeWrap.style.display = 'none';
|
||||
otsBadge.innerHTML = '';
|
||||
hideOtsCard('stamp');
|
||||
hideOtsCard('upgraded');
|
||||
summaryWrap.style.display = 'none';
|
||||
otsStampState = { valid: null, label: '', detail: '' };
|
||||
otsUpgradedState = { valid: null, label: '', detail: '' };
|
||||
|
||||
// Display full proof carrier JSON
|
||||
// Display full proof carrier JSON at the top of the page
|
||||
eventJsonEl.textContent = JSON.stringify(event, null, 2);
|
||||
eventJsonWrap.style.display = 'block';
|
||||
|
||||
@@ -231,9 +336,10 @@
|
||||
}
|
||||
|
||||
// 3. Inspect OTS proof tag and verify it matches the sha256 tag.
|
||||
// First do a quick structural check for immediate UI feedback, then
|
||||
// run full cryptographic verification (async — fetches Bitcoin block
|
||||
// headers and validates the Merkle path).
|
||||
// The OTS result is rendered as a dedicated card (pqOtsStampCard)
|
||||
// alongside the signature results. If the proof is pending, an
|
||||
// upgrade card (pqOtsUpgradedCard) is reserved for a later upgraded
|
||||
// proof. A final summary card is rendered at the end.
|
||||
const otsTag = event.tags.find(t => t[0] === 'ots');
|
||||
const sha256Tag = event.tags.find(t => t[0] === 'sha256');
|
||||
if (otsTag && otsTag[1]) {
|
||||
@@ -248,17 +354,27 @@
|
||||
: `WARNING: sha256 tag (${sha256Tag[1].substring(0, 16)}...) does not match computed hash (${fullHash.substring(0, 16)}...)`)
|
||||
: 'No sha256 tag found';
|
||||
|
||||
// Quick structural display first
|
||||
otsUpgradeWrap.style.display = 'block';
|
||||
|
||||
if (!validFile) {
|
||||
setOtsBadge('pq-ots-badge-none', 'OTS: Invalid format');
|
||||
setOtsInfo(`OTS tag present but does not appear to be a valid detached .ots file (${currentOtsBytes.length} bytes). ${hashMatchNote}.`);
|
||||
otsUpgradeWrap.style.display = 'none';
|
||||
} else if (hasBitcoinAttestation) {
|
||||
setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verifying...');
|
||||
setOtsInfo(`OpenTimestamps proof contains a Bitcoin attestation. Performing full cryptographic verification (fetching block header)... Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`);
|
||||
otsUpgradeWrap.style.display = 'block';
|
||||
// Invalid format — no upgrade possible
|
||||
otsStampState = { valid: false, label: '', detail: '' };
|
||||
setOtsCard('stamp', 'invalid', 'OTS Stamp: Invalid format',
|
||||
`OTS tag present but does not appear to be a valid detached .ots file (${currentOtsBytes.length} bytes). ${hashMatchNote}.`);
|
||||
hideOtsCard('upgraded');
|
||||
otsUpgradeBtn.style.display = 'none';
|
||||
otsRepublishBtn.style.display = 'none';
|
||||
renderSummary(allValid);
|
||||
} else if (hasBitcoinAttestation) {
|
||||
// Contains a Bitcoin attestation — run full async verification.
|
||||
// Show a "verifying" stamp card first, then update it.
|
||||
otsStampState = { valid: null, label: '', detail: '' };
|
||||
setOtsCard('stamp', 'pending', 'OTS Stamp: Verifying...',
|
||||
`OpenTimestamps proof contains a Bitcoin attestation. Performing full cryptographic verification (fetching block header)... Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`);
|
||||
hideOtsCard('upgraded');
|
||||
otsUpgradeBtn.style.display = 'none';
|
||||
otsRepublishBtn.style.display = 'none';
|
||||
renderSummary(allValid);
|
||||
|
||||
// Run full async verification: parse the proof, bind the target
|
||||
// digest to the sha256 tag, walk the Merkle path, and check the
|
||||
@@ -272,42 +388,117 @@
|
||||
: result.trustMode === 'single-explorer-checked'
|
||||
? 'single-explorer-checked (trusted API)'
|
||||
: result.trustMode || 'unknown';
|
||||
setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verified (Bitcoin)');
|
||||
setOtsInfo(`OpenTimestamps proof verified. Bitcoin block ${att.height} (mined ${date} UTC). Merkle root matches. Trust mode: ${trustLabel} — block header is trusted from explorer API(s), not independently verified against PoW. Proof commits to the event hash. Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`);
|
||||
otsStampState = {
|
||||
valid: true,
|
||||
label: `Bitcoin block ${att.height}`,
|
||||
detail: `OpenTimestamps proof verified. Bitcoin block ${att.height} (mined ${date} UTC). Merkle root matches. Trust mode: ${trustLabel} — block header is trusted from explorer API(s), not independently verified against PoW. Proof commits to the event hash. Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`
|
||||
};
|
||||
setOtsCard('stamp', 'valid', 'OTS Stamp: Verified (Bitcoin)', otsStampState.detail);
|
||||
otsUpgradeBtn.style.display = 'none';
|
||||
renderSummary(allValid);
|
||||
} else {
|
||||
setOtsBadge('pq-ots-badge-none', 'OTS: Verification failed');
|
||||
const errDetail = result.errors.length > 0 ? ` Errors: ${result.errors.join('; ')}` : '';
|
||||
setOtsInfo(`OpenTimestamps proof could NOT be cryptographically verified. ${hashMatchNote}.${errDetail}`);
|
||||
otsStampState = { valid: false, label: '', detail: '' };
|
||||
setOtsCard('stamp', 'invalid', 'OTS Stamp: Verification failed',
|
||||
`OpenTimestamps proof could NOT be cryptographically verified. ${hashMatchNote}.${errDetail}`);
|
||||
otsUpgradeBtn.style.display = 'inline-block';
|
||||
renderSummary(allValid);
|
||||
}
|
||||
}).catch(err => {
|
||||
setOtsBadge('pq-ots-badge-none', 'OTS: Verification error');
|
||||
setOtsInfo(`OpenTimestamps verification error: ${err.message}. ${hashMatchNote}.`);
|
||||
otsStampState = { valid: false, label: '', detail: '' };
|
||||
setOtsCard('stamp', 'invalid', 'OTS Stamp: Verification error',
|
||||
`OpenTimestamps verification error: ${err.message}. ${hashMatchNote}.`);
|
||||
otsUpgradeBtn.style.display = 'inline-block';
|
||||
renderSummary(allValid);
|
||||
});
|
||||
} else {
|
||||
setOtsBadge('pq-ots-badge-pending', 'OTS: Pending');
|
||||
setOtsInfo(`OpenTimestamps proof is pending (no Bitcoin attestation yet). Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}. You can try upgrading it below.`);
|
||||
otsUpgradeWrap.style.display = 'block';
|
||||
// Pending — no Bitcoin attestation yet. Reserve the upgraded card
|
||||
// for when the user upgrades the proof below.
|
||||
otsStampState = { valid: 'pending', label: '', detail: '' };
|
||||
setOtsCard('stamp', 'pending', 'OTS Stamp: Pending',
|
||||
`OpenTimestamps proof is pending (no Bitcoin attestation yet). Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}. You can try upgrading it below.`);
|
||||
hideOtsCard('upgraded');
|
||||
otsUpgradeBtn.style.display = 'inline-block';
|
||||
otsRepublishBtn.style.display = 'none';
|
||||
renderSummary(allValid);
|
||||
}
|
||||
} catch (e) {
|
||||
setOtsBadge('pq-ots-badge-none', 'OTS: Error');
|
||||
setOtsInfo(`Failed to parse OTS proof: ${e.message}`);
|
||||
otsUpgradeWrap.style.display = 'none';
|
||||
otsStampState = { valid: false, label: '', detail: '' };
|
||||
setOtsCard('stamp', 'invalid', 'OTS Stamp: Error',
|
||||
`Failed to parse OTS proof: ${e.message}`);
|
||||
hideOtsCard('upgraded');
|
||||
otsUpgradeBtn.style.display = 'none';
|
||||
otsRepublishBtn.style.display = 'none';
|
||||
renderSummary(allValid);
|
||||
}
|
||||
} else {
|
||||
setOtsBadge('pq-ots-badge-none', 'OTS: None');
|
||||
setOtsInfo('No OpenTimestamps proof tag found in this event.');
|
||||
otsStampState = { valid: false, label: '', detail: '' };
|
||||
setOtsCard('stamp', 'invalid', 'OTS Stamp: None',
|
||||
'No OpenTimestamps proof tag found in this event.');
|
||||
hideOtsCard('upgraded');
|
||||
otsUpgradeWrap.style.display = 'none';
|
||||
renderSummary(allValid);
|
||||
}
|
||||
|
||||
resultsWrap.style.display = 'block';
|
||||
// Only the event owner (signed-in pubkey matches event pubkey) may
|
||||
// upgrade/republish. Non-owners see the verification result but the
|
||||
// upgrade action is hidden to avoid confusion and endpoint abuse.
|
||||
applyOwnershipGate(event);
|
||||
return allValid;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
OWNERSHIP GATE: only the event owner can upgrade/republish the OTS
|
||||
proof. Non-owners see the verification result but the upgrade action
|
||||
buttons are hidden and replaced with an explanatory note.
|
||||
================================================================ */
|
||||
// Cache the signed-in pubkey so we don't call getPublicKey repeatedly.
|
||||
let cachedSignedInPubkey = null;
|
||||
let cachedSignedInPubkeyResolved = false;
|
||||
|
||||
async function getSignedInPubkey() {
|
||||
if (cachedSignedInPubkeyResolved) return cachedSignedInPubkey;
|
||||
cachedSignedInPubkeyResolved = true;
|
||||
if (!window.nostr || !window.nostr.getPublicKey) return null;
|
||||
try {
|
||||
cachedSignedInPubkey = await window.nostr.getPublicKey();
|
||||
} catch (e) {
|
||||
cachedSignedInPubkey = null;
|
||||
}
|
||||
return cachedSignedInPubkey;
|
||||
}
|
||||
|
||||
function applyOwnershipGate(event) {
|
||||
// Default: hide the upgrade action row until we confirm ownership.
|
||||
const actionRow = otsUpgradeWrap.querySelector('.pq-button-row');
|
||||
if (!actionRow) return;
|
||||
getSignedInPubkey().then(signedInPubkey => {
|
||||
const isOwner = signedInPubkey && event && event.pubkey &&
|
||||
signedInPubkey.toLowerCase() === event.pubkey.toLowerCase();
|
||||
if (isOwner) {
|
||||
actionRow.style.display = '';
|
||||
const note = document.getElementById('pqOtsOwnerNote');
|
||||
if (note) note.remove();
|
||||
} else {
|
||||
// Non-owner (or not signed in): hide the buttons and show a note.
|
||||
actionRow.style.display = 'none';
|
||||
let note = document.getElementById('pqOtsOwnerNote');
|
||||
if (!note) {
|
||||
note = document.createElement('div');
|
||||
note.id = 'pqOtsOwnerNote';
|
||||
note.className = 'pq-info-text';
|
||||
note.style.fontSize = '13px';
|
||||
note.style.marginTop = '8px';
|
||||
otsUpgradeWrap.appendChild(note);
|
||||
}
|
||||
note.textContent = signedInPubkey
|
||||
? 'Only the event owner can upgrade and republish the OpenTimestamps proof. You are signed in as a different identity.'
|
||||
: 'Only the event owner can upgrade and republish the OpenTimestamps proof. Sign in with the Nostr identity that published this event to enable upgrading.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
TAB 1: QUERY RELAY
|
||||
================================================================ */
|
||||
@@ -316,6 +507,7 @@
|
||||
|
||||
queryBtn.addEventListener('click', async () => {
|
||||
queryBtn.disabled = true;
|
||||
setButtonSpinner(queryBtn);
|
||||
setStatus(queryStatus, 'info', 'Querying relays...');
|
||||
|
||||
const pubkeyInput = document.getElementById('pqPubkeyInput').value.trim();
|
||||
@@ -325,6 +517,7 @@
|
||||
if (!pubkeyHex) {
|
||||
setStatus(queryStatus, 'error', 'Invalid pubkey. Enter a 64-char hex pubkey or an npub.');
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -332,6 +525,7 @@
|
||||
if (relayUrls.length === 0) {
|
||||
setStatus(queryStatus, 'error', 'Enter at least one relay URL.');
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -367,6 +561,7 @@
|
||||
if (allCandidates.length === 0) {
|
||||
setStatus(queryStatus, 'error', `No proof carrier events found for this pubkey on any of the ${relayUrls.length} relay(s)${lastError ? ' (last error: ' + lastError + ')' : ''}.`);
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -391,6 +586,7 @@
|
||||
if (!kind1Event) {
|
||||
setStatus(queryStatus, 'error', 'Could not parse kind 1 announcement from any candidate.');
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -410,7 +606,9 @@
|
||||
const trustModeNote = selection.canonicalTrustMode === 'pending-only'
|
||||
? ' ⚠ PENDING-ONLY — no Bitcoin attestation has been verified yet; this is not a trust decision.'
|
||||
: (selection.canonicalTrustMode === 'none' ? ' ⚠ no trustable anchor found.' : '');
|
||||
const statusClass = selection.canonicalTrustMode === 'pending-only' ? 'error' : 'success';
|
||||
// F-D4/L-2: a pending-only selection is informational, not an error —
|
||||
// it is not a trust decision, so do not render it in the error (red) style.
|
||||
const statusClass = selection.canonicalTrustMode === 'pending-only' ? 'info' : 'success';
|
||||
setStatus(queryStatus, statusClass, `Selected canonical proof carrier: ${selection.canonical.id.substring(0, 16)}... (${heightNote}). ${confirmedCount} confirmed, ${pendingCount} pending, ${allCandidates.length} total candidate(s).${trustModeNote}`);
|
||||
await verifyEvent(selection.canonical);
|
||||
} else {
|
||||
@@ -421,6 +619,7 @@
|
||||
}
|
||||
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
});
|
||||
|
||||
/* ================================================================
|
||||
@@ -489,22 +688,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
DOWNLOAD EVENT JSON
|
||||
================================================================ */
|
||||
downloadEventBtn.addEventListener('click', () => {
|
||||
if (!currentEvent) return;
|
||||
const blob = new Blob([JSON.stringify(currentEvent, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = (currentEvent.id ? currentEvent.id.substring(0, 16) : 'event') + '.json';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
/* ================================================================
|
||||
OTS UPGRADE (from verify page)
|
||||
================================================================ */
|
||||
@@ -533,10 +716,20 @@
|
||||
setStatus(otsUpgradeStatus, 'success', `Bitcoin attestation verified! Block ${att ? att.height : '?'} (mined ${date} UTC). Trust mode: ${trustLabel}. Proof upgraded (${currentOtsBytes.length} bytes). You can publish a new kind 9999 event carrying the confirmed proof below; it will reference the original via an upgrade_of tag.`);
|
||||
otsUpgradeBtn.style.display = 'none';
|
||||
otsRepublishBtn.style.display = 'inline-block';
|
||||
setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verified (Bitcoin)');
|
||||
setOtsInfo(`OpenTimestamps proof verified. Bitcoin block ${att ? att.height : '?'}. Trust mode: ${trustLabel}. Proof size: ${currentOtsBytes.length} bytes.`);
|
||||
// Render the upgraded proof as a second OTS card and refresh summary.
|
||||
otsUpgradedState = {
|
||||
valid: true,
|
||||
label: `Bitcoin block ${att ? att.height : '?'}`,
|
||||
detail: `OpenTimestamps proof verified. Bitcoin block ${att ? att.height : '?'} (mined ${date} UTC). Trust mode: ${trustLabel}. Proof size: ${currentOtsBytes.length} bytes.`
|
||||
};
|
||||
setOtsCard('upgraded', 'valid', 'OTS Upgraded: Verified (Bitcoin)', otsUpgradedState.detail);
|
||||
renderSummary(true);
|
||||
} else {
|
||||
setStatus(otsUpgradeStatus, 'info', `Proof upgraded but still pending (no Bitcoin attestation yet). ${result.detail ? 'Detail: ' + result.detail : ''} Try again later.`);
|
||||
otsUpgradedState = { valid: 'pending', label: '', detail: '' };
|
||||
setOtsCard('upgraded', 'pending', 'OTS Upgraded: Still pending',
|
||||
`Proof upgraded but still pending (no Bitcoin attestation yet). ${result.detail ? 'Detail: ' + result.detail : ''} Try again later.`);
|
||||
renderSummary(true);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(otsUpgradeStatus, 'error', `Upgrade failed: ${error.message}`);
|
||||
@@ -608,20 +801,168 @@
|
||||
});
|
||||
|
||||
/* ================================================================
|
||||
AUTO-DETECT SIGNED-IN USER
|
||||
On page load, check for a NIP-07 signer / nostr-login-lite. If the
|
||||
user is already signed in, pre-fill the pubkey field and auto-query
|
||||
relays for their kind 9999 event.
|
||||
AUTHENTICATION (nostr-login-lite / NIP-07)
|
||||
================================================================ */
|
||||
async function initNostrLoginLite() {
|
||||
if (!window.NOSTR_LOGIN_LITE) return false;
|
||||
if (!window.NOSTR_LOGIN_LITE._initialized) {
|
||||
await window.NOSTR_LOGIN_LITE.init({
|
||||
methods: { extension: true, local: true, nip46: true, seedphrase: true, nsigner: true }
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fill the pubkey field and auto-query relays for the signed-in user's
|
||||
// kind 9999 event. Switches to the Query Relay tab so the user sees the
|
||||
// results. Returns the pubkey hex on success, null otherwise.
|
||||
async function queryAsSignedInUser() {
|
||||
if (!window.nostr || !window.nostr.getPublicKey) {
|
||||
throw new Error('No Nostr signer available. Install a NIP-07 browser extension (nos2x, Alby) or use nostr-login-lite.');
|
||||
}
|
||||
const pubkeyHex = await window.nostr.getPublicKey();
|
||||
if (!pubkeyHex) throw new Error('No pubkey returned by signer.');
|
||||
|
||||
console.log('[verify] Signed-in pubkey:', pubkeyHex);
|
||||
const pubkeyInput = document.getElementById('pqPubkeyInput');
|
||||
const npub = hexToNpub(pubkeyHex);
|
||||
pubkeyInput.value = npub || pubkeyHex;
|
||||
|
||||
// Show the query tab so the user sees the results
|
||||
switchTab('relay');
|
||||
queryBtn.click();
|
||||
return pubkeyHex;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
TAB 3: SIGN IN
|
||||
================================================================ */
|
||||
const signInBtn = document.getElementById('pqSignInBtn');
|
||||
const signInError = document.getElementById('pqSignInError');
|
||||
const signInStatus = document.getElementById('pqSignInStatus');
|
||||
|
||||
signInBtn.addEventListener('click', async () => {
|
||||
signInBtn.disabled = true;
|
||||
signInError.innerHTML = '';
|
||||
setStatus(signInStatus, 'info', 'Signing in...');
|
||||
try {
|
||||
if (window.NOSTR_LOGIN_LITE) {
|
||||
await initNostrLoginLite();
|
||||
// Try a restored session first
|
||||
if (window.nostr) {
|
||||
try {
|
||||
const pk = await window.nostr.getPublicKey();
|
||||
if (pk) {
|
||||
await queryAsSignedInUser();
|
||||
setStatus(signInStatus, 'success', 'Signed in. Querying your event...');
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[verify] Not yet authenticated, launching login modal...');
|
||||
}
|
||||
}
|
||||
// Launch the login modal and wait for auth events
|
||||
const pubkeyHex = await new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
const timeout = setTimeout(() => {
|
||||
if (!settled) { settled = true; cleanup(); reject(new Error('Login timed out')); }
|
||||
}, 120000);
|
||||
|
||||
async function tryGetPubkey() {
|
||||
if (settled) return;
|
||||
if (window.nostr) {
|
||||
try {
|
||||
const pk = await window.nostr.getPublicKey();
|
||||
if (pk && !settled) { settled = true; cleanup(); resolve(pk); }
|
||||
} catch (e) { console.log('[verify] getPublicKey not ready:', e.message); }
|
||||
}
|
||||
}
|
||||
|
||||
function authHandler(e) {
|
||||
if (e.type === 'nlMethodSelected' && e.detail && e.detail.pubkey) {
|
||||
if (!settled) { settled = true; cleanup(); resolve(e.detail.pubkey); }
|
||||
return;
|
||||
}
|
||||
tryGetPubkey();
|
||||
setTimeout(tryGetPubkey, 200);
|
||||
setTimeout(tryGetPubkey, 500);
|
||||
setTimeout(tryGetPubkey, 1000);
|
||||
setTimeout(tryGetPubkey, 2000);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
window.removeEventListener('nlAuth', authHandler);
|
||||
window.removeEventListener('nlAuthRestored', authHandler);
|
||||
window.removeEventListener('nlMethodSelected', authHandler);
|
||||
window.removeEventListener('nlAuthComplete', authHandler);
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
window.addEventListener('nlAuth', authHandler);
|
||||
window.addEventListener('nlAuthRestored', authHandler);
|
||||
window.addEventListener('nlMethodSelected', authHandler);
|
||||
window.addEventListener('nlAuthComplete', authHandler);
|
||||
|
||||
try {
|
||||
const result = window.NOSTR_LOGIN_LITE.launch();
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(() => {
|
||||
tryGetPubkey(); setTimeout(tryGetPubkey, 200); setTimeout(tryGetPubkey, 500);
|
||||
}).catch(e => { if (!settled) { settled = true; cleanup(); reject(e); } });
|
||||
}
|
||||
} catch (e) {
|
||||
if (!settled) { settled = true; cleanup(); reject(e); }
|
||||
}
|
||||
});
|
||||
// Fill the pubkey field with the launched-session pubkey and query
|
||||
const pubkeyInput = document.getElementById('pqPubkeyInput');
|
||||
const npub = hexToNpub(pubkeyHex);
|
||||
pubkeyInput.value = npub || pubkeyHex;
|
||||
switchTab('relay');
|
||||
queryBtn.click();
|
||||
setStatus(signInStatus, 'success', 'Signed in. Querying your event...');
|
||||
} else {
|
||||
// No nostr-login-lite — fall back to a plain NIP-07 extension
|
||||
await queryAsSignedInUser();
|
||||
setStatus(signInStatus, 'success', 'Signed in. Querying your event...');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[verify] Sign-in failed:', error);
|
||||
setStatus(signInError, 'error', error.message);
|
||||
setStatus(signInStatus, 'error', 'Sign-in failed.');
|
||||
} finally {
|
||||
signInBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
/* ================================================================
|
||||
URL ?npub= PARAMETER + AUTO-DETECT SIGNED-IN USER
|
||||
On page load, first check for a ?npub= query parameter (used by the
|
||||
"Verify this attestation" link in the kind 1 event). If present,
|
||||
pre-fill the pubkey field and auto-query relays for that pubkey's
|
||||
kind 9999 event. Otherwise, fall back to detecting a NIP-07 signer /
|
||||
nostr-login-lite session and auto-query for the signed-in user.
|
||||
================================================================ */
|
||||
async function initAutoDetect() {
|
||||
// Initialize nostr-login-lite if available (shares session with main page)
|
||||
// 1. Check for ?npub= (or ?pubkey=) URL parameter first
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const npubParam = params.get('npub') || params.get('pubkey');
|
||||
if (npubParam) {
|
||||
const pubkeyHex = normalizePubkey(npubParam);
|
||||
if (pubkeyHex) {
|
||||
console.log('[verify] URL parameter pubkey:', pubkeyHex);
|
||||
const pubkeyInput = document.getElementById('pqPubkeyInput');
|
||||
const npub = hexToNpub(pubkeyHex);
|
||||
pubkeyInput.value = npub || pubkeyHex;
|
||||
queryBtn.click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fall back to signed-in user detection
|
||||
if (window.NOSTR_LOGIN_LITE) {
|
||||
try {
|
||||
if (!window.NOSTR_LOGIN_LITE._initialized) {
|
||||
await window.NOSTR_LOGIN_LITE.init({
|
||||
methods: { extension: true, local: true, nip46: true, seedphrase: true, nsigner: true }
|
||||
});
|
||||
}
|
||||
await initNostrLoginLite();
|
||||
} catch (e) {
|
||||
console.log('[verify] nostr-login-lite init failed:', e.message);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/**
|
||||
* Version display: fetch version.json and update title + header.
|
||||
* Version display: fetch version.json and render a small, unobtrusive
|
||||
* version badge fixed to the bottom-right of the page.
|
||||
* Extracted from inline <script> for CSP compliance (G56-08).
|
||||
*
|
||||
* Auto-detects the page title prefix from the existing <title> tag.
|
||||
* The page <title> is still suffixed with the version for tab/bookmark
|
||||
* context, but the in-page header keeps only the human-readable title.
|
||||
*/
|
||||
(function () {
|
||||
// Derive the title prefix from the existing <title> element
|
||||
@@ -13,9 +15,16 @@
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (v) {
|
||||
var versionText = v.VERSION || '';
|
||||
// Suffix the browser tab/bookmark title with the version.
|
||||
document.title = titlePrefix + ' ' + versionText;
|
||||
var header = document.getElementById('divHeaderText');
|
||||
if (header) header.textContent = titlePrefix + ' ' + versionText;
|
||||
|
||||
// Render a small fixed version badge in the bottom-right corner.
|
||||
if (versionText) {
|
||||
var badge = document.createElement('div');
|
||||
badge.className = 'pq-version-badge';
|
||||
badge.textContent = versionText;
|
||||
document.body.appendChild(badge);
|
||||
}
|
||||
})
|
||||
.catch(function () { /* version.json not found — keep default title */ });
|
||||
})();
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.0.34",
|
||||
"VERSION_NUMBER": "0.0.34",
|
||||
"BUILD_DATE": "2026-07-27T16:53:32.439Z"
|
||||
"VERSION": "v0.1.6",
|
||||
"VERSION_NUMBER": "0.1.6",
|
||||
"BUILD_DATE": "2026-07-30T10:46:55.076Z"
|
||||
}
|
||||
|
||||
@@ -9929,7 +9929,8 @@ Post-quantum public keys in tags:
|
||||
|
||||
Each post-quantum key has cryptographically signed this attestation. This event is pending timestamp on the Bitcoin blockchain via OpenTimestamps.
|
||||
|
||||
Verify this attestation: https://laantungir.net/quantum-prep/verify.html
|
||||
Verify this attestation: https://laantungir.net/quantum-prep/verify.html?npub=${npub}
|
||||
|
||||
Created at: https://laantungir.net/quantum-prep/`;
|
||||
const statementBytes = new TextEncoder().encode(content);
|
||||
const mlDsa44Sig = signWithMLDSA44(statementBytes, pqKeys.mlDsa44.secretKey);
|
||||
|
||||
+69
-45
@@ -69,6 +69,22 @@
|
||||
.pq-button:hover { opacity: 0.7; }
|
||||
.pq-button:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
|
||||
/* Braille spinner shown inside a button while its action is running.
|
||||
Frames are advanced by JS (setButtonSpinner); this class just styles
|
||||
the inline spinner glyph. */
|
||||
.pq-btn-spinner {
|
||||
display: inline-block;
|
||||
color: var(--accent-color);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Keep the spinner fully visible even while the button is dimmed
|
||||
(disabled) during a running action. */
|
||||
.pq-button:disabled .pq-btn-spinner { opacity: 1; }
|
||||
|
||||
.pq-button-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -164,6 +180,31 @@
|
||||
border-left: 4px solid #cc0000;
|
||||
}
|
||||
|
||||
.pq-result-pending {
|
||||
border-left: 4px solid #f0ad4e;
|
||||
}
|
||||
|
||||
/* Multi-line OTS / summary cards wrap their detail text. */
|
||||
.pq-result-item.pq-result-detail {
|
||||
display: block;
|
||||
line-height: 1.6;
|
||||
white-space: normal;
|
||||
}
|
||||
.pq-result-detail .pq-result-head {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pq-result-detail .pq-result-body {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.pq-summary-card {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding: 15px 18px;
|
||||
}
|
||||
|
||||
.pq-event-preview {
|
||||
background: var(--secondary-color);
|
||||
color: var(--primary-color);
|
||||
@@ -211,30 +252,6 @@
|
||||
|
||||
.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>
|
||||
|
||||
@@ -258,6 +275,7 @@
|
||||
<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 class="pq-tab" id="pqTabSignIn">Sign In</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB 1: Query Relay -->
|
||||
@@ -302,27 +320,35 @@
|
||||
<input type="file" id="pqArchiveFileInput" accept="application/json,.json" style="margin-top: 6px; font-size: 13px;" />
|
||||
</div>
|
||||
|
||||
<!-- Shared results area -->
|
||||
<!-- TAB 3: SIGN IN -->
|
||||
<div class="pq-tab-panel" id="pqPanelSignIn">
|
||||
<div class="pq-info-text">
|
||||
Sign in with your Nostr identity (NIP-07 extension or nostr-login-lite) to look up your
|
||||
own kind 9999 event automatically.
|
||||
</div>
|
||||
<div id="pqSignInError"></div>
|
||||
<button class="pq-button" id="pqSignInBtn">Sign In</button>
|
||||
<div id="pqSignInStatus"></div>
|
||||
</div>
|
||||
|
||||
<!-- Full event JSON (shown at the top once an event is loaded) -->
|
||||
<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>
|
||||
|
||||
<!-- Shared results area: signature + PQ checks + OTS cards, all in
|
||||
one continuous card list. The OTS stamp/upgraded cards are
|
||||
appended into pqResultList by JS so the flow is unbroken. -->
|
||||
<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>
|
||||
<!-- OTS upgrade actions (cards live in pqResultList above) -->
|
||||
<div id="pqOtsUpgradeWrap" style="display: none; margin-top: 10px;">
|
||||
<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>
|
||||
@@ -330,6 +356,11 @@
|
||||
<div id="pqOtsUpgradeStatus"></div>
|
||||
</div>
|
||||
|
||||
<!-- Summary card (shown once verification completes) -->
|
||||
<div id="pqSummaryWrap" style="display: none; margin-top: 15px;">
|
||||
<div class="pq-result-item pq-summary-card" id="pqSummaryCard"></div>
|
||||
</div>
|
||||
|
||||
<hr style="border: var(--border); margin: 25px 0 15px;" />
|
||||
|
||||
<div class="pq-info-text" style="color: var(--accent-color); font-size: 13px;">
|
||||
@@ -345,13 +376,6 @@
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user