Joey (NostrComments)'s avatar
Joey (NostrComments)
slurpnc@coinos.io
npub1ewxm...ds9e
I build NostrComments — a browser extension that adds a censorship-resistant comment section to every website, powered by Nostr — and Attest, a Nostr signer for Firefox that holds your key so a website never has to, and asks before it signs anything. Free and open source, always. NostrComments for Firefox: https://addons.mozilla.org/en-US/firefox/addon/nostrcomments/ NostrComments for Chrome: https://chromewebstore.google.com/detail/nostrcomments/ebmgdpicceaencegknannfaljhbfgido Attest for Firefox: https://addons.mozilla.org/firefox/addon/attest/ Monero: 87aDTPD9HQx2QenKsS7MvHDdqsziFPD7UB37X6G5XVXc2ZPhAs8DdEKUPYJijVcRjj1gU5KvxLCTfWUKWqrd1D5o8uw5EpM
If your browser extension uses sender.tab to tell web pages apart from its own pages, check this. I got it wrong in a signer and it cost me a release. The background script refused privileged messages, like setting a PIN, whenever sender.tab was set. The reasoning: content scripts run in tabs, so a message with a tab came from a web page. But the extension's own options page has a tab too, as soon as it is opened in one. So the extension blocked itself. PIN setup didn't throw an error or show a warning. It just did nothing. The check that means what you intended is the sender's URL, not whether a tab exists: compare sender.url against browser.runtime.getURL(''). Your own pages pass, and anything a content script relays from a web page does not. The second half of the same fix: the content-script bridge now forwards only an explicit list of message types. Before, some handlers were out of a page's reach only by accident, and an accident is not a boundary.
NostrComments 23.4.0 is out. A reader of the extension — Emre Yılmaz ( @Emre Yilmaz ), blog.emre.xyz — wrote up a discovery convention for pages that are also native Nostr content: a <link rel="alternate" href="nostr:naddr…"> tag, so a client can find the entity a page already is. Sent it in as a proper draft, trust considerations included. Built the read half of it — a page naming its own Nostr entity now has that thread merged into the panel, alongside the URL-scoped one. Thank you for taking the time to write it up properly Emre!
If you use nos2x-fox as your Nostr signer in Firefox, make sure you're on 1.21.0. It closes two things worth knowing about. While your PIN was cached after an unlock, any website could ask the extension for it and get it back in plain text. That PIN is what encrypts your private key at rest, so a page could walk away with the one secret between it and your key. I reported it privately and wrote the fix; it was also reported publicly as #68. The second was found by daym. The extension used to inject its script into pages as <script src="moz-extension://<uuid>/…">. Firefox gives every extension a random UUID per install precisely so websites can't fingerprint you by the extensions you run, and that tag handed it to every site you visited: the same on each one, and surviving cleared cookies. It now loads as a MAIN-world content script, with nothing left in the page to read. Nothing to do except update. Firefox normally does that for you; about:addons shows which version you have.
One of the dumbest things you can do on Nostr is sharing your private keys, like nsec138mwgc6rvpukzn7xj5522xzc63h69f6533tupl25trtps4nckl0qp452vg
If your browser extension draws its UI inside the page, check this one. I had it wrong for months. My comment panel lives in an open shadow root in the page, so any script on the page can reach every control in it. I guarded the ones that matter — granting consent, adding a relay — by checking the event's isTrusted, which only the browser can set. That felt like enough. It is not, and I only found out because I tried to break it myself instead of assuming. A page can skip the event entirely: take the element, call its onclick directly, and hand it an object it made up. isTrusted is just a property on that object. It can also act while a real click of yours is being delivered elsewhere in the panel, which is long enough for a naive "was that trusted" flag to still be true. Against my own build that meant a page could open settings, press "Show private key" and read the nsec straight out of the field. And separately, type a comment and post it under my key. Without me clicking anything. What works instead is to never trust what the caller passed. A capture listener on the shadow root records which element a real gesture is activating, in a closure the page cannot reach, and each sensitive handler asks only whether that element is itself. Dispatching an event sets nothing. Calling a handler sends no event at all. Piggybacking on a real click leaves some other element as the target. It is still a mitigation and not a boundary, and I would rather say so: the panel is in the page's DOM, so a page can read the thread, read what I am typing, and read a key I choose to display while I am on it. Moving those controls onto the extension's own surface is the actual fix, and that is the next thing I build. Fixed in 23.3.1 — out on Firefox, in review at Chrome.
Ever wanted to reply to an article on the internet that has comments switched off? 👀 I built NostrComments for that. It's a browser extension that gives every web page a comment section, powered by Nostr. Here's the idea: your comment doesn't live on the website. It lives on Nostr relays, linked to the page's address. So the website can't delete it, and anyone else with NostrComments who visits that page can see it and reply. A few nice things: ✍ Use your own Nostr key, or keep it safe in a signer like Alby, Attest or nos2x 🤝 Comments on web pages made with other Nostr apps, like Amethyst and Ditto, show up too 🔌 No company server in the middle. You pick your relays 🙋 It stays quiet until you say yes, and you can turn it off for any site ⚡ Vote, mute and zap, right in the panel Curious but don't want to install anything? Read the comments on any page here: Quick demo (66 sec): 🦊 Firefox: 🌐 Chrome: https://chromewebstore.google.com/detail/nostrcomments/ebmgdpicceaencegknannfaljhbfgido 💻 Open source: It's still early, so most pages have no comments yet. If you try it, leave one! That's how every conversation starts 💜 #nostr #grownostr #opensource #foss #privacy #nip22
You can now read a NostrComments thread without installing anything: That's a Nature article with no comment section, and five comments filed under its address on public relays. Paste any URL to see what has been said about it. Static page, no server, no backend — it queries the same relays your own client does, from your browser. Source: #nostr #privacy #nostrcomments
NostrComments has a proper demo now — 66 seconds, no sales pitch: A comment thread on any page, attached to the page's address and stored on relays instead of on the site. The clip shows it on a Nature article with no comment section, the same thread appearing in a second browser under a different key, and then a paywalled paper, a corporate announcement, an EU page and a post on X. Also out: 23.2.2, where the toolbar button opens and closes the panel. That matters because a page can delete the floating button — the toolbar is outside its reach. Firefox: Chrome: https://chromewebstore.google.com/detail/nostrcomments/ebmgdpicceaencegknannfaljhbfgido
Attest is a Nostr signer for Firefox: it holds your key and asks before anything is signed. What's new in recent releases: • Protect your key with a passphrase, not just a 4–6 digit PIN. A short PIN stops someone using your browser; it does not stop someone who copies your Firefox profile, and the extension now says so plainly instead of implying otherwise. • Permissions are tied to a site's full address, https or http. A site you allowed over https is no longer also allowed over plain http. • Each site keeps one decision per kind of permission, so refusing something briefly no longer wipes a permission you granted permanently. • The options page shows what is actually there: no empty key field for a key that is encrypted, and adding another key happens where you'd look for it. Open source, reproducible build, and the release refuses to ship if the tests fail or if the code that handles your keys doesn't type-check.
NostrComments 23.2.0 is out 🧵 It now works on sites that used to show an empty panel. Some sites (x.com is the best-known) use a strict Content-Security-Policy that blocks connections to anywhere they haven't listed, and Firefox applied that rule to the extension too, so no relay could be reached. The extension now opens its relay connections from its own background, where the site has no say. Also new: • a switch to leave the "client" tag off your comments • on Firefox, the welcome screen now suggests Attest instead of nos2x-fox, which has a public flaw that lets a website read the PIN protecting your key. Attest is my fork with that fixed. What it is: a comment thread on any URL, stored on relays instead of on the site, so the site owner can't delete it. Comments are NIP-22 (kind 1111), so other clients can read them. Firefox: Chrome/Brave/Edge: https://chromewebstore.google.com/detail/nostrcomments/ebmgdpicceaencegknannfaljhbfgido Source: No server, no account, no tracking. Feedback welcome, here or in the thread on any page. #nostr #privacy image
A browser extension can be talked to by two very different things: its own pages, and a content script sitting in somebody's web page. Telling them apart is the whole security boundary, and the obvious way to do it is wrong. `sender.tab` looks like the answer. It is not. An options page opened with tabs.create() has a tab like any web page does, so checking for one makes the extension refuse its own UI — and it fails silently, because the message goes out, comes back refused, and nothing on screen says why. A button that does nothing. `sender.url` against runtime.getURL('') is the one that holds. A content script always reports the address of the page it was injected into, never a moz-extension:// one, and content scripts do not run on extension pages. Cost me a release before I found it.
I hand-rolled secp256k1 and BIP-340 Schnorr for a browser extension, and I would like someone who does this properly to read it. Why hand-rolled: the extension ships with no build step and no dependencies, so the file you install is the file in the repository, comparable line for line. A library would have cost that. Whether the trade was worth it is part of what I am asking. What I have already done. It passes all nineteen official BIP-340 test vectors, including the ten that must be rejected — public key off the curve, sig[0:32] not an x coordinate, s equal to the curve order, sG - eP at infinity. I mutation-tested that suite too: removing the even-y check on R is caught by exactly one assertion in the entire project, vector #6, and by nothing else. What I already know is weak. The scalar multiplication is a plain double-and-add branching on secret bits, so it runs in variable time. I do not think it is reachable from a hostile page — a content script's signing cannot be triggered or observed from the page — but I would rather say it than have it pointed out. And BigInt is not constant time in any JS engine, so this is not something I can fully fix in this language. What testing cannot tell me: whether the field arithmetic is right. Vectors prove the implementation behaves on inputs somebody thought to write down. They say nothing about the modular inverse. It is forty lines. I am not asking anyone for a free audit — just for eyes. I would rather find out now than after somebody depends on it.
↑