Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
399a633697 | ||
|
|
a662686a0b | ||
|
|
668fb65efb | ||
|
|
270cddc433 | ||
|
|
33e06adc4a | ||
|
|
b46d081211 | ||
|
|
57d8d2fcf4 | ||
|
|
79185f9909 | ||
|
|
94f03f2999 | ||
|
|
200eb41f39 |
@@ -204,12 +204,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 +294,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 +340,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.33",
|
||||
"version": "0.1.5",
|
||||
"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);
|
||||
|
||||
+17
-27
@@ -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>
|
||||
@@ -633,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>
|
||||
|
||||
+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);
|
||||
|
||||
+405
-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,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;
|
||||
}
|
||||
|
||||
@@ -421,6 +617,7 @@
|
||||
}
|
||||
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
});
|
||||
|
||||
/* ================================================================
|
||||
@@ -489,22 +686,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 +714,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 +799,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.33",
|
||||
"VERSION_NUMBER": "0.0.33",
|
||||
"BUILD_DATE": "2026-07-26T15:58:51.892Z"
|
||||
"VERSION": "v0.1.5",
|
||||
"VERSION_NUMBER": "0.1.5",
|
||||
"BUILD_DATE": "2026-07-29T13:52:32.430Z"
|
||||
}
|
||||
|
||||
@@ -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
-64
@@ -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,45 +320,62 @@
|
||||
<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>
|
||||
</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