Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33e06adc4a | ||
|
|
b46d081211 | ||
|
|
57d8d2fcf4 | ||
|
|
79185f9909 | ||
|
|
94f03f2999 | ||
|
|
200eb41f39 | ||
|
|
ed572e21e5 | ||
|
|
8039af473a |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nostr_quantum_preparation",
|
||||
"version": "0.0.31",
|
||||
"version": "0.1.1",
|
||||
"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": {
|
||||
|
||||
+22
-15
@@ -104,6 +104,7 @@
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
.pq-checklist-link {
|
||||
color: var(--accent-color);
|
||||
text-decoration: underline;
|
||||
@@ -355,14 +356,14 @@
|
||||
<li>A new seed phrase. This should be kept secret and private.</li>
|
||||
<li>A proof archive. This information is published to relays, but keeping a copy yourself is useful in case the relays don't keep the event.</li>
|
||||
</ol>
|
||||
In case quantum computers break nostr, you can use your seed phrase to create a new nostr
|
||||
|
||||
</div>
|
||||
|
||||
<div class="pq-info-text">
|
||||
In case quantum computers break nostr, you can use your new seed phrase to create a new nostr
|
||||
identity, and prove that your old nostr identity created this new one.
|
||||
</div>
|
||||
<div class="pq-info-text" style="font-size: 13px; color: var(--accent-color);">
|
||||
<strong>Warning:</strong> Don't enter a valuable existing seed phrase. This tool doesn't make
|
||||
your Nostr identity post-quantum secure on its own — it creates a public link your future self
|
||||
can prove is yours. Run through it once with a test identity before you use it on your real one.
|
||||
</div>
|
||||
|
||||
<div class="pq-info-text" id="noSignerWarning" style="color: var(--accent-color); display: none;">
|
||||
No Nostr signer detected. Install a NIP-07 browser extension (like nos2x or Alby) and reload.
|
||||
</div>
|
||||
@@ -452,6 +453,11 @@
|
||||
|
||||
<!-- Bring your own mode -->
|
||||
<div id="pqSeedOwnPanel" style="display: none;">
|
||||
<div class="pq-info-text" style="font-size: 13px; color: var(--accent-color);">
|
||||
<strong>Warning:</strong> Don't enter a valuable existing seed phrase. This tool doesn't make
|
||||
your Nostr identity post-quantum secure on its own — it creates a public link your future self
|
||||
can prove is yours. Run through it once with a test identity before you use it on your real one.
|
||||
</div>
|
||||
<div class="pq-info-text">
|
||||
Paste your 12 or 24-word BIP39 seed phrase below. It will be validated and used to
|
||||
derive your post-quantum keys. Make sure you generated it offline or with a trusted tool.
|
||||
@@ -543,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>
|
||||
@@ -600,7 +603,7 @@
|
||||
<span>Pending proof embedded in kind 9999 event</span>
|
||||
</div>
|
||||
<div class="pq-checklist-item pq-active" id="pqOtsConfirm">
|
||||
<span class="pq-checklist-box"></span>
|
||||
<span class="pq-checklist-box" id="pqOtsConfirmBox"></span>
|
||||
<span>Wait for Bitcoin confirmation
|
||||
<div class="pq-key-meta" id="pqOtsConfirmDetail" style="margin-top: 2px;">Polling every 60 seconds...</div>
|
||||
</span>
|
||||
@@ -610,11 +613,15 @@
|
||||
<span>Publish new kind 9999 with confirmed proof — automatic on confirmation</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pqOtsProofWrap" class="pq-hidden" style="margin-top: 15px;">
|
||||
<div class="pq-info-text" style="margin-bottom: 5px;">OTS proof file (base64):</div>
|
||||
<div class="pq-event-preview" id="pqOtsProof" style="max-height: 150px;"></div>
|
||||
<div class="pq-button-row" style="margin-top: 10px;">
|
||||
<button class="pq-button pq-button-secondary" id="pqOtsDownloadBtn">Download .ots file</button>
|
||||
<div id="pqOtsArchiveWrap" class="pq-hidden" style="margin-top: 15px;">
|
||||
<div class="pq-info-text" style="margin-bottom: 8px;">
|
||||
Bitcoin confirmation verified and the upgraded proof carrier has been published to relays.
|
||||
Download the upgraded proof archive below — it contains the kind 1 event, the upgraded kind 9999
|
||||
proof carrier, and the <strong>confirmed</strong> OTS proof (with Bitcoin attestation). Keep a copy
|
||||
in case relays delete the event.
|
||||
</div>
|
||||
<div class="pq-button-row">
|
||||
<button class="pq-button pq-button-secondary" id="pqOtsArchiveBtn">Download Upgraded Proof Archive</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+103
-31
@@ -16,6 +16,7 @@
|
||||
bytesToBase64,
|
||||
base64ToBytes,
|
||||
bytesToHex,
|
||||
hexToNpub,
|
||||
PQ_KEY_INFO,
|
||||
NIP_QR_KIND,
|
||||
buildUpgradedEvent,
|
||||
@@ -140,6 +141,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Braille spinner on the "Wait for Bitcoin confirmation" checklist box
|
||||
(pqOtsConfirm). Same frames as the step-6 checklist-box spinner. */
|
||||
let otsConfirmSpinnerInterval = null;
|
||||
function startOtsConfirmSpinner() {
|
||||
const box = document.getElementById('pqOtsConfirmBox');
|
||||
if (!box) return;
|
||||
stopOtsConfirmSpinner();
|
||||
box.classList.add('pq-spinner');
|
||||
let frame = 0;
|
||||
box.textContent = SPINNER_FRAMES[0];
|
||||
otsConfirmSpinnerInterval = setInterval(() => {
|
||||
frame = (frame + 1) % SPINNER_FRAMES.length;
|
||||
box.textContent = SPINNER_FRAMES[frame];
|
||||
}, 80);
|
||||
}
|
||||
function stopOtsConfirmSpinner() {
|
||||
if (otsConfirmSpinnerInterval) { clearInterval(otsConfirmSpinnerInterval); otsConfirmSpinnerInterval = null; }
|
||||
const box = document.getElementById('pqOtsConfirmBox');
|
||||
if (box) { box.classList.remove('pq-spinner'); box.textContent = ''; }
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
VIEW MANAGEMENT
|
||||
================================================================ */
|
||||
@@ -1029,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 () => {
|
||||
@@ -1046,6 +1058,7 @@
|
||||
// Stop OTS polling, but preserve its persisted workflow for the next login.
|
||||
if (otsPollInterval) { clearInterval(otsPollInterval); otsPollInterval = null; }
|
||||
clearStepSpinner(6);
|
||||
stopOtsConfirmSpinner();
|
||||
pendingOtsBytes = null;
|
||||
const persistedOts = localStorage.getItem('pq-pending-ots');
|
||||
// Logout from nostr-login-lite
|
||||
@@ -1142,6 +1155,8 @@
|
||||
if (otsLogEl) otsLogEl.innerHTML = '';
|
||||
setKeyIcon('pqOtsConfirm', '');
|
||||
setKeyIcon('pqOtsConfirmedPublish', '');
|
||||
stopOtsConfirmSpinner();
|
||||
hideUpgradedArchiveButton();
|
||||
|
||||
// Check for a saved workflow from a previous session (same event ID)
|
||||
let existing = loadPendingOts();
|
||||
@@ -1160,10 +1175,10 @@
|
||||
setStepDone(6);
|
||||
setStepActive(7);
|
||||
otsStatus.innerHTML = '<div class="pq-status pq-status-success">Confirmed timestamp proof was already published.</div>';
|
||||
showOtsProof(pendingOtsBytes);
|
||||
showUpgradedArchiveButton();
|
||||
return;
|
||||
}
|
||||
showOtsProof(pendingOtsBytes);
|
||||
hideUpgradedArchiveButton();
|
||||
setKeyIcon('pqOtsPendingPublish', 'Done');
|
||||
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof is embedded in the kind 9999 event. Waiting for Bitcoin confirmation...</div>';
|
||||
startOtsPolling();
|
||||
@@ -1178,7 +1193,6 @@
|
||||
pendingOtsBytes = base64ToBytes(otsTag[1]);
|
||||
if (isDetachedOtsFile(pendingOtsBytes)) {
|
||||
otsLog(`Found pending OTS proof embedded in event (${pendingOtsBytes.length} bytes).`);
|
||||
showOtsProof(pendingOtsBytes);
|
||||
savePendingOts(pqEvent.id, pendingOtsBytes, {
|
||||
ownerPubkey: currentPubkey,
|
||||
targetKind: pqEvent.kind,
|
||||
@@ -1214,8 +1228,6 @@
|
||||
const otsBytes = await timestampEvent(fullHash);
|
||||
pendingOtsBytes = otsBytes;
|
||||
otsLog(`Created pending detached .ots file (${otsBytes.length} bytes).`);
|
||||
showOtsProof(otsBytes);
|
||||
|
||||
// Republish the event with the OTS proof embedded
|
||||
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Publishing new kind 9999 event with embedded OTS proof...</div>';
|
||||
await publishUpgradedEvent(otsBytes);
|
||||
@@ -1238,20 +1250,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
function showOtsProof(otsBytes) {
|
||||
const proofWrap = document.getElementById('pqOtsProofWrap');
|
||||
const proofEl = document.getElementById('pqOtsProof');
|
||||
proofEl.textContent = bytesToBase64(otsBytes);
|
||||
proofWrap.classList.remove('pq-hidden');
|
||||
// The raw OTS proof base64 display and standalone .ots download have been
|
||||
// removed from step 6. Instead, after confirmation + publish, the user is
|
||||
// offered an upgraded proof archive (kind 1 + upgraded kind 9999 + confirmed
|
||||
// OTS proof) via showUpgradedArchiveButton().
|
||||
function showUpgradedArchiveButton() {
|
||||
const wrap = document.getElementById('pqOtsArchiveWrap');
|
||||
if (wrap) wrap.classList.remove('pq-hidden');
|
||||
}
|
||||
function hideUpgradedArchiveButton() {
|
||||
const wrap = document.getElementById('pqOtsArchiveWrap');
|
||||
if (wrap) wrap.classList.add('pq-hidden');
|
||||
}
|
||||
|
||||
function startOtsPolling() {
|
||||
const confirmDetail = document.getElementById('pqOtsConfirmDetail');
|
||||
let pollCount = 0;
|
||||
if (otsPollInterval) clearInterval(otsPollInterval);
|
||||
// Show a braille spinner in the step-6 checklist box so the user can
|
||||
// see the page is still actively working while waiting for Bitcoin.
|
||||
// Show a braille spinner in the step-6 checklist box and inline next
|
||||
// to the "Wait for Bitcoin confirmation" label so the user can see
|
||||
// the page is still actively working while waiting for Bitcoin.
|
||||
setStepSpinner(6);
|
||||
startOtsConfirmSpinner();
|
||||
|
||||
async function poll() {
|
||||
pollCount++;
|
||||
@@ -1266,8 +1286,6 @@
|
||||
proofCarrierEvent: pqEvent,
|
||||
kind1Event
|
||||
});
|
||||
showOtsProof(pendingOtsBytes);
|
||||
|
||||
// Run full cryptographic verification: parse the proof, bind the
|
||||
// target digest to the event's sha256 tag, walk the Merkle path,
|
||||
// and check the block header against a Bitcoin API.
|
||||
@@ -1284,13 +1302,14 @@
|
||||
: verifyResult.trustMode || 'unknown';
|
||||
otsLog(`Poll ${pollCount}: Bitcoin attestation VERIFIED (block ${att ? att.height : '?'}, mined ${date} UTC, trust: ${trustLabel}). Proof: ${pendingOtsBytes.length} bytes.`);
|
||||
setKeyIcon('pqOtsConfirm', 'Done');
|
||||
stopOtsConfirmSpinner();
|
||||
confirmDetail.textContent = `Verified on Bitcoin (block ${att ? att.height : '?'}, ${trustLabel})`;
|
||||
if (otsPollInterval) { clearInterval(otsPollInterval); otsPollInterval = null; }
|
||||
|
||||
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
|
||||
@@ -1322,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...');
|
||||
@@ -1357,9 +1376,50 @@
|
||||
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);
|
||||
showUpgradedArchiveButton();
|
||||
return validatedEvent;
|
||||
} catch (error) {
|
||||
otsLog(`ERROR publishing upgraded event: ${error.message}`);
|
||||
@@ -1368,16 +1428,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
// OTS event listeners
|
||||
document.getElementById('pqOtsDownloadBtn').addEventListener('click', () => {
|
||||
if (pendingOtsBytes) {
|
||||
const blob = new Blob([pendingOtsBytes], { type: 'application/octet-stream' });
|
||||
// OTS event listener: download the upgraded proof archive (kind 1 event +
|
||||
// upgraded kind 9999 proof carrier + confirmed OTS proof) after Bitcoin
|
||||
// confirmation and publish.
|
||||
document.getElementById('pqOtsArchiveBtn').addEventListener('click', () => {
|
||||
if (!kind1Event || !pqEvent || !pendingOtsBytes) {
|
||||
console.warn('[post-quantum] Cannot build upgraded archive: missing kind1Event, proofCarrier, or OTS proof');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const archiveJson = buildProofArchive(kind1Event, pqEvent, pendingOtsBytes);
|
||||
const blob = new Blob([archiveJson], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${pqEvent.id.substring(0, 16)}.ots`;
|
||||
a.download = `nostr-pq-proof-${pqEvent.id.substring(0, 12)}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
console.log('[post-quantum] Upgraded proof archive downloaded');
|
||||
} catch (e) {
|
||||
console.error('[post-quantum] Failed to build upgraded archive:', e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+350
-66
@@ -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,36 +388,56 @@
|
||||
: 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';
|
||||
@@ -316,6 +452,7 @@
|
||||
|
||||
queryBtn.addEventListener('click', async () => {
|
||||
queryBtn.disabled = true;
|
||||
setButtonSpinner(queryBtn);
|
||||
setStatus(queryStatus, 'info', 'Querying relays...');
|
||||
|
||||
const pubkeyInput = document.getElementById('pqPubkeyInput').value.trim();
|
||||
@@ -325,6 +462,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 +470,7 @@
|
||||
if (relayUrls.length === 0) {
|
||||
setStatus(queryStatus, 'error', 'Enter at least one relay URL.');
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -367,6 +506,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 +531,7 @@
|
||||
if (!kind1Event) {
|
||||
setStatus(queryStatus, 'error', 'Could not parse kind 1 announcement from any candidate.');
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -421,6 +562,7 @@
|
||||
}
|
||||
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
});
|
||||
|
||||
/* ================================================================
|
||||
@@ -489,22 +631,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 +659,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 +744,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);
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.0.31",
|
||||
"VERSION_NUMBER": "0.0.31",
|
||||
"BUILD_DATE": "2026-07-26T15:44:07.425Z"
|
||||
"VERSION": "v0.1.1",
|
||||
"VERSION_NUMBER": "0.1.1",
|
||||
"BUILD_DATE": "2026-07-29T13:00:25.910Z"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+82
-57
@@ -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>
|
||||
|
||||
@@ -254,35 +271,19 @@
|
||||
|
||||
<div class="pq-card">
|
||||
<div class="pq-card-title">Verify Post-Quantum Key Link</div>
|
||||
<div class="pq-info-text" style="color: var(--accent-color); font-size: 13px;">
|
||||
<strong>Note:</strong> Passing these checks confirms the keys are linked and the signatures
|
||||
are valid — it does not mean the identity is post-quantum secure. The Bitcoin timestamp check
|
||||
relies on a public block explorer rather than running a full Bitcoin node, so a compromised
|
||||
explorer could fool it.
|
||||
</div>
|
||||
<div class="pq-info-text">
|
||||
You can check a post-quantum key-link event two ways: look it up on a relay by pubkey, or
|
||||
paste the event JSON directly. The event you're checking (kind 9999) wraps a public
|
||||
announcement (kind 1) and carries a Bitcoin timestamp proof. Verification checks each piece
|
||||
separately and reports the results below: both Nostr signatures, the link between the two
|
||||
events, each post-quantum signature (ML-DSA-44, ML-DSA-65, SLH-DSA-128s, Falcon-512 —
|
||||
ML-KEM-768 is for encryption, so it has no signature to check), that all required algorithms
|
||||
are present, that the event really belongs to the claimed identity, and the Bitcoin timestamp.
|
||||
</div>
|
||||
<div class="pq-info-text">
|
||||
<a href="./" class="pq-link">Back to preparation 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 class="pq-tab" id="pqTabSignIn">Sign In</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.
|
||||
relay(s) for the latest kind 9999 event from that pubkey and verify the signatures are
|
||||
valid, and whether it has been submitted to Open Time Stamps.
|
||||
</div>
|
||||
<input type="text" class="pq-input" id="pqPubkeyInput"
|
||||
placeholder="Pubkey (hex or npub), e.g. 3bf0c63f869a8972... or npub1..." />
|
||||
@@ -319,33 +320,57 @@
|
||||
<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>
|
||||
</div>
|
||||
<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;">
|
||||
<strong>Note:</strong> Passing every check does not mean that your current nostr identity
|
||||
is now post-quantum secure. This tool is the preparation step. Full quantum safety for
|
||||
day-to-day Nostr use still requires companion protocols that are not yet built.
|
||||
</div>
|
||||
<div class="pq-info-text">
|
||||
<a href="./" class="pq-link">Back to preparation page</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user