Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b46d081211 | ||
|
|
57d8d2fcf4 | ||
|
|
79185f9909 | ||
|
|
94f03f2999 | ||
|
|
200eb41f39 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nostr_quantum_preparation",
|
||||
"version": "0.0.33",
|
||||
"version": "0.1.0",
|
||||
"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": {
|
||||
|
||||
+17
-21
@@ -104,16 +104,6 @@
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
/* Inline braille spinner shown next to the "Wait for Bitcoin
|
||||
confirmation" label. Frames advanced by JS (startInlineSpinner). */
|
||||
.pq-inline-spinner {
|
||||
display: inline-block;
|
||||
color: var(--accent-color);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
margin-right: 6px;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
|
||||
.pq-checklist-link {
|
||||
color: var(--accent-color);
|
||||
@@ -366,9 +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" 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>
|
||||
@@ -554,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>
|
||||
@@ -611,8 +603,8 @@
|
||||
<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><span class="pq-inline-spinner" id="pqOtsConfirmSpinner"></span>Wait for Bitcoin confirmation
|
||||
<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>
|
||||
</div>
|
||||
@@ -621,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>
|
||||
|
||||
+94
-47
@@ -16,6 +16,7 @@
|
||||
bytesToBase64,
|
||||
base64ToBytes,
|
||||
bytesToHex,
|
||||
hexToNpub,
|
||||
PQ_KEY_INFO,
|
||||
NIP_QR_KIND,
|
||||
buildUpgradedEvent,
|
||||
@@ -140,24 +141,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Inline braille spinner next to the "Wait for Bitcoin confirmation"
|
||||
label. Same frames as the checklist-box spinner. */
|
||||
let inlineSpinnerInterval = null;
|
||||
function startInlineSpinner(elementId) {
|
||||
const el = document.getElementById(elementId);
|
||||
if (!el) return;
|
||||
stopInlineSpinner(elementId);
|
||||
/* 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;
|
||||
el.textContent = SPINNER_FRAMES[0];
|
||||
inlineSpinnerInterval = setInterval(() => {
|
||||
box.textContent = SPINNER_FRAMES[0];
|
||||
otsConfirmSpinnerInterval = setInterval(() => {
|
||||
frame = (frame + 1) % SPINNER_FRAMES.length;
|
||||
el.textContent = SPINNER_FRAMES[frame];
|
||||
box.textContent = SPINNER_FRAMES[frame];
|
||||
}, 80);
|
||||
}
|
||||
function stopInlineSpinner() {
|
||||
if (inlineSpinnerInterval) { clearInterval(inlineSpinnerInterval); inlineSpinnerInterval = null; }
|
||||
const el = document.getElementById('pqOtsConfirmSpinner');
|
||||
if (el) el.textContent = '';
|
||||
function stopOtsConfirmSpinner() {
|
||||
if (otsConfirmSpinnerInterval) { clearInterval(otsConfirmSpinnerInterval); otsConfirmSpinnerInterval = null; }
|
||||
const box = document.getElementById('pqOtsConfirmBox');
|
||||
if (box) { box.classList.remove('pq-spinner'); box.textContent = ''; }
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
@@ -1049,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 () => {
|
||||
@@ -1066,7 +1058,7 @@
|
||||
// Stop OTS polling, but preserve its persisted workflow for the next login.
|
||||
if (otsPollInterval) { clearInterval(otsPollInterval); otsPollInterval = null; }
|
||||
clearStepSpinner(6);
|
||||
stopInlineSpinner();
|
||||
stopOtsConfirmSpinner();
|
||||
pendingOtsBytes = null;
|
||||
const persistedOts = localStorage.getItem('pq-pending-ots');
|
||||
// Logout from nostr-login-lite
|
||||
@@ -1163,7 +1155,8 @@
|
||||
if (otsLogEl) otsLogEl.innerHTML = '';
|
||||
setKeyIcon('pqOtsConfirm', '');
|
||||
setKeyIcon('pqOtsConfirmedPublish', '');
|
||||
stopInlineSpinner();
|
||||
stopOtsConfirmSpinner();
|
||||
hideUpgradedArchiveButton();
|
||||
|
||||
// Check for a saved workflow from a previous session (same event ID)
|
||||
let existing = loadPendingOts();
|
||||
@@ -1182,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();
|
||||
@@ -1200,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,
|
||||
@@ -1236,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);
|
||||
@@ -1260,11 +1250,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -1275,7 +1271,7 @@
|
||||
// to the "Wait for Bitcoin confirmation" label so the user can see
|
||||
// the page is still actively working while waiting for Bitcoin.
|
||||
setStepSpinner(6);
|
||||
startInlineSpinner('pqOtsConfirmSpinner');
|
||||
startOtsConfirmSpinner();
|
||||
|
||||
async function poll() {
|
||||
pollCount++;
|
||||
@@ -1290,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.
|
||||
@@ -1308,14 +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');
|
||||
stopInlineSpinner();
|
||||
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
|
||||
@@ -1347,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...');
|
||||
@@ -1382,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}`);
|
||||
@@ -1393,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);
|
||||
|
||||
+167
-29
@@ -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,7 +49,6 @@
|
||||
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');
|
||||
@@ -489,22 +495,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)
|
||||
================================================================ */
|
||||
@@ -608,20 +598,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.33",
|
||||
"VERSION_NUMBER": "0.0.33",
|
||||
"BUILD_DATE": "2026-07-26T15:58:51.892Z"
|
||||
"VERSION": "v0.1.0",
|
||||
"VERSION_NUMBER": "0.1.0",
|
||||
"BUILD_DATE": "2026-07-28T10:01:19.383Z"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+25
-22
@@ -254,35 +254,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,6 +303,17 @@
|
||||
<input type="file" id="pqArchiveFileInput" accept="application/json,.json" style="margin-top: 6px; font-size: 13px;" />
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Shared results area -->
|
||||
<div id="pqResultsWrap" style="display: none;">
|
||||
<div class="pq-info-text" style="margin-top: 15px; margin-bottom: 5px;">
|
||||
@@ -331,9 +326,6 @@
|
||||
<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 -->
|
||||
@@ -346,6 +338,17 @@
|
||||
</div>
|
||||
<div id="pqOtsUpgradeStatus"></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