Compare commits

...
4 Commits
8 changed files with 107 additions and 29 deletions
+20 -8
View File
@@ -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 ~2030 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
View File
@@ -1,6 +1,6 @@
{
"name": "nostr_quantum_preparation",
"version": "0.1.1",
"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": {
+15
View File
@@ -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);
-6
View File
@@ -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>
+55
View File
@@ -441,9 +441,64 @@
}
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
================================================================ */
+13 -4
View File
@@ -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
View File
@@ -1,5 +1,5 @@
{
"VERSION": "v0.1.1",
"VERSION_NUMBER": "0.1.1",
"BUILD_DATE": "2026-07-29T13:00:25.910Z"
"VERSION": "v0.1.5",
"VERSION_NUMBER": "0.1.5",
"BUILD_DATE": "2026-07-29T13:52:32.430Z"
}
-7
View File
@@ -376,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>