Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a662686a0b | ||
|
|
668fb65efb | ||
|
|
270cddc433 | ||
|
|
33e06adc4a | ||
|
|
b46d081211 | ||
|
|
57d8d2fcf4 |
@@ -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.36",
|
||||
"version": "0.1.4",
|
||||
"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);
|
||||
|
||||
@@ -629,12 +629,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="divFooter">
|
||||
<div id="divFooterLeft" class="divFooterBox"></div>
|
||||
<div id="divFooterCenter" class="divFooterBox"></div>
|
||||
<div id="divFooterRight" class="divFooterBox"></div>
|
||||
</div>
|
||||
|
||||
<!-- SCRIPTS -->
|
||||
<script src="/nostr-login-lite/nostr.bundle.js" integrity="sha384-LNnPDD++DaWxljIhMLmfaoEoKB0B1HmACC6gNGLG+Qnmosprf/AX7u84pVY8xMpM" crossorigin="anonymous"></script>
|
||||
<script src="/nostr-login-lite/nostr-lite.js" integrity="sha384-IQwa65eDC5trGjKn4cuEazmFiHTHD65gIqsjpzDtouc+j0MlyI927SZgHMWSi2aC" crossorigin="anonymous"></script>
|
||||
|
||||
+43
-2
@@ -16,6 +16,7 @@
|
||||
bytesToBase64,
|
||||
base64ToBytes,
|
||||
bytesToHex,
|
||||
hexToNpub,
|
||||
PQ_KEY_INFO,
|
||||
NIP_QR_KIND,
|
||||
buildUpgradedEvent,
|
||||
@@ -1308,7 +1309,7 @@
|
||||
const saved = loadPendingOts();
|
||||
if (!saved || !saved.confirmedPublished) {
|
||||
setStatus(document.getElementById('pqOtsStatus'), 'success', 'Bitcoin attestation verified. Publishing new kind 9999 with confirmed proof...');
|
||||
await publishUpgradedEvent(pendingOtsBytes);
|
||||
await publishUpgradedEvent(pendingOtsBytes, att ? att.height : null, att ? att.time : null);
|
||||
}
|
||||
} else if (isOtsConfirmed(pendingOtsBytes)) {
|
||||
// Structural check found a Bitcoin attestation tag but full
|
||||
@@ -1340,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...');
|
||||
@@ -1375,6 +1376,46 @@
|
||||
kind1Event
|
||||
});
|
||||
|
||||
// Publish a kind 1 reply announcing the confirmed attestation.
|
||||
// This is a regular Nostr text note (reply to the original kind 1
|
||||
// announcement) so it shows up in feeds/thread views. It only needs
|
||||
// the secp256k1 signature — no PQ signatures.
|
||||
if (kind1Event && kind1Event.id) {
|
||||
try {
|
||||
otsLog('Publishing kind 1 reply announcing confirmed attestation...');
|
||||
const npub = hexToNpub(currentPubkey);
|
||||
const dateStr = bitcoinTime
|
||||
? new Date(bitcoinTime * 1000).toISOString().substring(0, 19)
|
||||
: 'unknown';
|
||||
const blockStr = bitcoinHeight ? `Bitcoin block ${bitcoinHeight} (mined ${dateStr} UTC)` : 'a Bitcoin block';
|
||||
const replyContent = `✅ The OpenTimestamps proof for my post-quantum key attestation has been confirmed in ${blockStr}. The link between my current Nostr identity and my post-quantum keys is now anchored to the Bitcoin blockchain and cannot be backdated.
|
||||
|
||||
Verify it here: https://laantungir.net/quantum-prep/verify.html?npub=${npub}
|
||||
|
||||
This is a reply to my original attestation announcement. The confirmed proof is carried in a new kind 9999 event referencing the original.`;
|
||||
const replyTemplate = {
|
||||
kind: 1,
|
||||
content: replyContent,
|
||||
tags: [
|
||||
['e', kind1Event.id, '', 'root'],
|
||||
['e', validatedEvent.id, '', 'mention']
|
||||
],
|
||||
pubkey: currentPubkey,
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
const replySigned = await window.nostr.signEvent(replyTemplate);
|
||||
const replyValidated = validateSignerOutput(replySigned, replyTemplate, currentPubkey);
|
||||
const replyResults = await publishToRelays(replyValidated, relayUrls);
|
||||
const replyOk = replyResults.filter(r => r.success).length;
|
||||
const replyFail = replyResults.filter(r => !r.success).length;
|
||||
otsLog(`Kind 1 reply published. Successful: ${replyOk}; failed: ${replyFail}.`);
|
||||
} catch (replyErr) {
|
||||
// The upgraded proof carrier is already published; a failed reply
|
||||
// is non-fatal — log it but don't fail the whole operation.
|
||||
otsLog(`WARNING: kind 1 reply failed: ${replyErr.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
setStatus(otsStatus, 'success', `Upgraded proof carrier published to ${successCount} relays. Timestamping complete.`);
|
||||
setStepDone(6);
|
||||
setStepActive(7);
|
||||
|
||||
+183
-37
@@ -50,17 +50,55 @@
|
||||
const resultList = document.getElementById('pqResultList');
|
||||
const eventJsonWrap = document.getElementById('pqEventJsonWrap');
|
||||
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');
|
||||
@@ -77,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.';
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
@@ -174,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';
|
||||
|
||||
@@ -237,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]) {
|
||||
@@ -254,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
|
||||
@@ -278,36 +388,56 @@
|
||||
: result.trustMode === 'single-explorer-checked'
|
||||
? 'single-explorer-checked (trusted API)'
|
||||
: result.trustMode || 'unknown';
|
||||
setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verified (Bitcoin)');
|
||||
setOtsInfo(`OpenTimestamps proof verified. Bitcoin block ${att.height} (mined ${date} UTC). Merkle root matches. Trust mode: ${trustLabel} — block header is trusted from explorer API(s), not independently verified against PoW. Proof commits to the event hash. Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`);
|
||||
otsStampState = {
|
||||
valid: true,
|
||||
label: `Bitcoin block ${att.height}`,
|
||||
detail: `OpenTimestamps proof verified. Bitcoin block ${att.height} (mined ${date} UTC). Merkle root matches. Trust mode: ${trustLabel} — block header is trusted from explorer API(s), not independently verified against PoW. Proof commits to the event hash. Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`
|
||||
};
|
||||
setOtsCard('stamp', 'valid', 'OTS Stamp: Verified (Bitcoin)', otsStampState.detail);
|
||||
otsUpgradeBtn.style.display = 'none';
|
||||
renderSummary(allValid);
|
||||
} else {
|
||||
setOtsBadge('pq-ots-badge-none', 'OTS: Verification failed');
|
||||
const errDetail = result.errors.length > 0 ? ` Errors: ${result.errors.join('; ')}` : '';
|
||||
setOtsInfo(`OpenTimestamps proof could NOT be cryptographically verified. ${hashMatchNote}.${errDetail}`);
|
||||
otsStampState = { valid: false, label: '', detail: '' };
|
||||
setOtsCard('stamp', 'invalid', 'OTS Stamp: Verification failed',
|
||||
`OpenTimestamps proof could NOT be cryptographically verified. ${hashMatchNote}.${errDetail}`);
|
||||
otsUpgradeBtn.style.display = 'inline-block';
|
||||
renderSummary(allValid);
|
||||
}
|
||||
}).catch(err => {
|
||||
setOtsBadge('pq-ots-badge-none', 'OTS: Verification error');
|
||||
setOtsInfo(`OpenTimestamps verification error: ${err.message}. ${hashMatchNote}.`);
|
||||
otsStampState = { valid: false, label: '', detail: '' };
|
||||
setOtsCard('stamp', 'invalid', 'OTS Stamp: Verification error',
|
||||
`OpenTimestamps verification error: ${err.message}. ${hashMatchNote}.`);
|
||||
otsUpgradeBtn.style.display = 'inline-block';
|
||||
renderSummary(allValid);
|
||||
});
|
||||
} else {
|
||||
setOtsBadge('pq-ots-badge-pending', 'OTS: Pending');
|
||||
setOtsInfo(`OpenTimestamps proof is pending (no Bitcoin attestation yet). Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}. You can try upgrading it below.`);
|
||||
otsUpgradeWrap.style.display = 'block';
|
||||
// Pending — no Bitcoin attestation yet. Reserve the upgraded card
|
||||
// for when the user upgrades the proof below.
|
||||
otsStampState = { valid: 'pending', label: '', detail: '' };
|
||||
setOtsCard('stamp', 'pending', 'OTS Stamp: Pending',
|
||||
`OpenTimestamps proof is pending (no Bitcoin attestation yet). Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}. You can try upgrading it below.`);
|
||||
hideOtsCard('upgraded');
|
||||
otsUpgradeBtn.style.display = 'inline-block';
|
||||
otsRepublishBtn.style.display = 'none';
|
||||
renderSummary(allValid);
|
||||
}
|
||||
} catch (e) {
|
||||
setOtsBadge('pq-ots-badge-none', 'OTS: Error');
|
||||
setOtsInfo(`Failed to parse OTS proof: ${e.message}`);
|
||||
otsUpgradeWrap.style.display = 'none';
|
||||
otsStampState = { valid: false, label: '', detail: '' };
|
||||
setOtsCard('stamp', 'invalid', 'OTS Stamp: Error',
|
||||
`Failed to parse OTS proof: ${e.message}`);
|
||||
hideOtsCard('upgraded');
|
||||
otsUpgradeBtn.style.display = 'none';
|
||||
otsRepublishBtn.style.display = 'none';
|
||||
renderSummary(allValid);
|
||||
}
|
||||
} else {
|
||||
setOtsBadge('pq-ots-badge-none', 'OTS: None');
|
||||
setOtsInfo('No OpenTimestamps proof tag found in this event.');
|
||||
otsStampState = { valid: false, label: '', detail: '' };
|
||||
setOtsCard('stamp', 'invalid', 'OTS Stamp: None',
|
||||
'No OpenTimestamps proof tag found in this event.');
|
||||
hideOtsCard('upgraded');
|
||||
otsUpgradeWrap.style.display = 'none';
|
||||
renderSummary(allValid);
|
||||
}
|
||||
|
||||
resultsWrap.style.display = 'block';
|
||||
@@ -322,6 +452,7 @@
|
||||
|
||||
queryBtn.addEventListener('click', async () => {
|
||||
queryBtn.disabled = true;
|
||||
setButtonSpinner(queryBtn);
|
||||
setStatus(queryStatus, 'info', 'Querying relays...');
|
||||
|
||||
const pubkeyInput = document.getElementById('pqPubkeyInput').value.trim();
|
||||
@@ -331,6 +462,7 @@
|
||||
if (!pubkeyHex) {
|
||||
setStatus(queryStatus, 'error', 'Invalid pubkey. Enter a 64-char hex pubkey or an npub.');
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,6 +470,7 @@
|
||||
if (relayUrls.length === 0) {
|
||||
setStatus(queryStatus, 'error', 'Enter at least one relay URL.');
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -373,6 +506,7 @@
|
||||
if (allCandidates.length === 0) {
|
||||
setStatus(queryStatus, 'error', `No proof carrier events found for this pubkey on any of the ${relayUrls.length} relay(s)${lastError ? ' (last error: ' + lastError + ')' : ''}.`);
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -397,6 +531,7 @@
|
||||
if (!kind1Event) {
|
||||
setStatus(queryStatus, 'error', 'Could not parse kind 1 announcement from any candidate.');
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -427,6 +562,7 @@
|
||||
}
|
||||
|
||||
queryBtn.disabled = false;
|
||||
clearButtonSpinner(queryBtn);
|
||||
});
|
||||
|
||||
/* ================================================================
|
||||
@@ -523,10 +659,20 @@
|
||||
setStatus(otsUpgradeStatus, 'success', `Bitcoin attestation verified! Block ${att ? att.height : '?'} (mined ${date} UTC). Trust mode: ${trustLabel}. Proof upgraded (${currentOtsBytes.length} bytes). You can publish a new kind 9999 event carrying the confirmed proof below; it will reference the original via an upgrade_of tag.`);
|
||||
otsUpgradeBtn.style.display = 'none';
|
||||
otsRepublishBtn.style.display = 'inline-block';
|
||||
setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verified (Bitcoin)');
|
||||
setOtsInfo(`OpenTimestamps proof verified. Bitcoin block ${att ? att.height : '?'}. Trust mode: ${trustLabel}. Proof size: ${currentOtsBytes.length} bytes.`);
|
||||
// Render the upgraded proof as a second OTS card and refresh summary.
|
||||
otsUpgradedState = {
|
||||
valid: true,
|
||||
label: `Bitcoin block ${att ? att.height : '?'}`,
|
||||
detail: `OpenTimestamps proof verified. Bitcoin block ${att ? att.height : '?'} (mined ${date} UTC). Trust mode: ${trustLabel}. Proof size: ${currentOtsBytes.length} bytes.`
|
||||
};
|
||||
setOtsCard('upgraded', 'valid', 'OTS Upgraded: Verified (Bitcoin)', otsUpgradedState.detail);
|
||||
renderSummary(true);
|
||||
} else {
|
||||
setStatus(otsUpgradeStatus, 'info', `Proof upgraded but still pending (no Bitcoin attestation yet). ${result.detail ? 'Detail: ' + result.detail : ''} Try again later.`);
|
||||
otsUpgradedState = { valid: 'pending', label: '', detail: '' };
|
||||
setOtsCard('upgraded', 'pending', 'OTS Upgraded: Still pending',
|
||||
`Proof upgraded but still pending (no Bitcoin attestation yet). ${result.detail ? 'Detail: ' + result.detail : ''} Try again later.`);
|
||||
renderSummary(true);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(otsUpgradeStatus, 'error', `Upgrade failed: ${error.message}`);
|
||||
|
||||
@@ -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.36",
|
||||
"VERSION_NUMBER": "0.0.36",
|
||||
"BUILD_DATE": "2026-07-27T20:31:02.763Z"
|
||||
"VERSION": "v0.1.4",
|
||||
"VERSION_NUMBER": "0.1.4",
|
||||
"BUILD_DATE": "2026-07-29T13:21:03.946Z"
|
||||
}
|
||||
|
||||
+59
-44
@@ -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>
|
||||
|
||||
@@ -314,24 +331,24 @@
|
||||
<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;">
|
||||
<strong>Verification Results:</strong>
|
||||
<span id="pqOtsBadge"></span>
|
||||
</div>
|
||||
<div class="pq-result-list" id="pqResultList"></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>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
</div>
|
||||
<div class="pq-result-list" id="pqResultList"></div>
|
||||
</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>
|
||||
@@ -339,6 +356,11 @@
|
||||
<div id="pqOtsUpgradeStatus"></div>
|
||||
</div>
|
||||
|
||||
<!-- Summary card (shown once verification completes) -->
|
||||
<div id="pqSummaryWrap" style="display: none; margin-top: 15px;">
|
||||
<div class="pq-result-item pq-summary-card" id="pqSummaryCard"></div>
|
||||
</div>
|
||||
|
||||
<hr style="border: var(--border); margin: 25px 0 15px;" />
|
||||
|
||||
<div class="pq-info-text" style="color: var(--accent-color); font-size: 13px;">
|
||||
@@ -354,13 +376,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div id="divFooter">
|
||||
<div id="divFooterLeft" class="divFooterBox"></div>
|
||||
<div id="divFooterCenter" class="divFooterBox"></div>
|
||||
<div id="divFooterRight" class="divFooterBox"></div>
|
||||
</div>
|
||||
|
||||
<!-- SCRIPTS -->
|
||||
<script src="/nostr-login-lite/nostr.bundle.js" integrity="sha384-LNnPDD++DaWxljIhMLmfaoEoKB0B1HmACC6gNGLG+Qnmosprf/AX7u84pVY8xMpM" crossorigin="anonymous"></script>
|
||||
<script src="/nostr-login-lite/nostr-lite.js" integrity="sha384-IQwa65eDC5trGjKn4cuEazmFiHTHD65gIqsjpzDtouc+j0MlyI927SZgHMWSi2aC" crossorigin="anonymous"></script>
|
||||
|
||||
Reference in New Issue
Block a user